Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
146240 stories
·
33 followers

Wolverine Idioms for MediatR Users

1 Share

The Wolverine community fields a lot of questions from people who are moving to Wolverine from their previous MediatR usage. A quite natural response is to try to use Wolverine as a pure drop in replacement for MediatR and even try to use the existing MediatR idioms they’re already used to. However, Wolverine comes from a different philosophy than MediatR and most of the other “mediator” tools it’s inspired and using Wolverine with its idioms might lead to much simpler code or more efficient execution. Inspired by a conversation I had online today, let’s just into an example that I think shows quite a bit of contrast between the tools.

We’ve tried to lay out some of the differences between the tools in our Wolverine for MediatR Users guide, including the section this post is taken from.

Here’s an example of MediatR usage I borrowed from this blog post that shows the usage of MediatR within a shopping cart subsystem:

public class AddToCartRequest : IRequest<Result>
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
public class AddToCartHandler : IRequestHandler<AddToCartRequest, Result>
{
private readonly ICartService _cartService;
public AddToCartHandler(ICartService cartService)
{
_cartService = cartService;
}
public async Task<Result> Handle(AddToCartRequest request, CancellationToken cancellationToken)
{
// Logic to add the product to the cart using the cart service
bool addToCartResult = await _cartService.AddToCart(request.ProductId, request.Quantity);
bool isAddToCartSuccessful = addToCartResult; // Check if adding the product to the cart was successful.
return Result.SuccessIf(isAddToCartSuccessful, "Failed to add the product to the cart."); // Return failure if adding to cart fails.
}
}
public class CartController : ControllerBase
{
private readonly IMediator _mediator;
public CartController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> AddToCart([FromBody] AddToCartRequest request)
{
var result = await _mediator.Send(request);
if (result.IsSuccess)
{
return Ok("Product added to the cart successfully.");
}
else
{
return BadRequest(result.ErrorMessage);
}
}
}

Note the usage of the custom Result<T> type from the message handler. Folks using MediatR love using these custom Result types when you’re passing information between logical layers because it avoids the usage of throwing exceptions and communicates failure cases more clearly.

See Andrew Lock on Working with the result pattern for more information about the Result pattern.

Wolverine is all about reducing code ceremony and we always strive to write application code as synchronous pure functions whenever possible, so let’s just write the exact same functionality as above using Wolverine idioms to shrink down the code:

public static class AddToCartRequestEndpoint
{
// Remember, we can do validation in middleware, or
// even do a custom Validate() : ProblemDetails method
// to act as a filter so the main method is the happy path
[WolverinePost("/api/cart/add"), EmptyResponse]
public static Update<Cart> Post(
AddToCartRequest request,
// This usage will return a 400 status code if the Cart
// cannot be found
[Entity(OnMissing = OnMissing.ProblemDetailsWith400)] Cart cart)
{
return cart.TryAddRequest(request) ? Storage.Update(cart) : Storage.Nothing(cart);
}
}

There’s a lot going on above, so let’s dive into some of the details:

I used Wolverine.HTTP to write the HTTP endpoint so we only have one piece of code for our “vertical slice” instead of having both the Controller method and the matching message handler for the same logical command. Wolverine.HTTP embraces our Railway Programming model and direct support for the ProblemDetails specification as a means of stopping the HTTP request such that validation pre-conditions can be validated by middleware such that the main endpoint method is really the “happy path”.

The code above is using Wolverine’s “declarative data access” helpers you see in the [Entity] usage. We realized early on that a lot of message handlers or HTTP endpoints need to work on a single domain entity or a handful of entities loaded by identity values riding on either command messages, HTTP requests, or HTTP routes. At runtime, if the Cart isn’t found by loading it from your configured application persistence (which could be EF Core, Marten, or RavenDb at this time), the whole HTTP request would stop with status code 400 and a message communicated through ProblemDetails that the requested Cart cannot be found.

The key point I’m trying to prove is that idiomatic Wolverine results in potentially less repetitive code, less code ceremony, and less layering than MediatR idioms. Sure, it’s going to take a bit to get used to Wolverine idioms, but the potential payoff is code that’s easier to reason about and much easier to unit test — especially if you’ll buy into our A-Frame Architecture approach for organizing code within your slices.

Validation Middleware

As another example just to show how Wolverine’s runtime is different than MediatR’s, let’s consider the very common case of using Fluent Validation (or now DataAnnotations too!) middleware in front of message handlers or HTTP requests. With MediatR, you might use an IPipelineBehavior<T> implementation like this that will wrap all requests:

    public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
        private readonly IEnumerable<IValidator<TRequest>> _validators;
        public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
        {
            _validators = validators;
        }
      
        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            if (_validators.Any())
            {
                var context = new ValidationContext<TRequest>(request);
                var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
                var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
                if (failures.Count != 0)
                    throw new ValidationException(failures);
            }
          
            return await next();
        }
    }

    I’ve seen plenty of alternatives out there with slightly different implementations. In some cases folks will use service location to probe the application’s IoC container for any possible IValidator<T> implementations for the current request. In all cases though, the implementations are using runtime logic on every possible request to check if there is any validation logic. With the Wolverine version of Fluent Validation middleware, we do things a bit differently with less runtime overhead that will also result in cleaner Exception stack traces when things go wrong — don’t laugh, we really did design Wolverine quite purposely to avoid the really nasty kind of Exception stack traces you get from many other middleware or “behavior” using frameworks like Wolverine’s predecessor tool FubuMVC did 😦

    Let’s say that you have a Wolverine.HTTP endpoint like so:

    public record CreateCustomer
    (
    string FirstName,
    string LastName,
    string PostalCode
    )
    {
    public class CreateCustomerValidator : AbstractValidator<CreateCustomer>
    {
    public CreateCustomerValidator()
    {
    RuleFor(x => x.FirstName).NotNull();
    RuleFor(x => x.LastName).NotNull();
    RuleFor(x => x.PostalCode).NotNull();
    }
    }
    }
    public static class CreateCustomerEndpoint
    {
    [WolverinePost("/validate/customer")]
    public static string Post(CreateCustomer customer)
    {
    return "Got a new customer";
    }
    [WolverinePost("/validate/customer2")]
    public static string Post2([FromQuery] CreateCustomer customer)
    {
    return "Got a new customer";
    }
    }

    In the application bootstrapping, I’ve added this option:

    app.MapWolverineEndpoints(opts =>
    {
    // more configuration for HTTP...
    // Opting into the Fluent Validation middleware from
    // Wolverine.Http.FluentValidation
    opts.UseFluentValidationProblemDetailMiddleware();
    }

    Just like with MediatR, you would need to register the Fluent Validation validator types in your IoC container as part of application bootstrapping. Now, here’s how Wolverine’s model is very different from MediatR’s pipeline behaviors. While MediatR is applying that ValidationBehaviour to each and every message handler in your application whether or not that message type actually has any registered validators, Wolverine is able to peek into the IoC configuration and “know” whether there are registered validators for any given message type. If there are any registered validators, Wolverine will utilize them in the code it generates to execute the HTTP endpoint method shown above for creating a customer. If there is only one validator, and that validator is registered as a Singleton scope in the IoC container, Wolverine generates this code:

        public class POST_validate_customer : Wolverine.Http.HttpHandler
        {
            private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
            private readonly Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> _problemDetailSource;
            private readonly FluentValidation.IValidator<WolverineWebApi.Validation.CreateCustomer> _validator;
    
            public POST_validate_customer(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource<WolverineWebApi.Validation.CreateCustomer> problemDetailSource, FluentValidation.IValidator<WolverineWebApi.Validation.CreateCustomer> validator) : base(wolverineHttpOptions)
            {
                _wolverineHttpOptions = wolverineHttpOptions;
                _problemDetailSource = problemDetailSource;
                _validator = validator;
            }
    
    
    
            public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
            {
                // Reading the request body via JSON deserialization
                var (customer, jsonContinue) = await ReadJsonAsync<WolverineWebApi.Validation.CreateCustomer>(httpContext);
                if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
                
                // Execute FluentValidation validators
                var result1 = await Wolverine.Http.FluentValidation.Internals.FluentValidationHttpExecutor.ExecuteOne<WolverineWebApi.Validation.CreateCustomer>(_validator, _problemDetailSource, customer).ConfigureAwait(false);
    
                // Evaluate whether or not the execution should be stopped based on the IResult value
                if (result1 != null && !(result1 is Wolverine.Http.WolverineContinue))
                {
                    await result1.ExecuteAsync(httpContext).ConfigureAwait(false);
                    return;
                }
    
    
                
                // The actual HTTP request handler execution
                var result_of_Post = WolverineWebApi.Validation.ValidatedEndpoint.Post(customer);
    
                await WriteString(httpContext, result_of_Post);
            }
    
        }

    I should note that Wolverine’s Fluent Validation middleware will not generate any code for any HTTP endpoint where there are no known Fluent Validation validators for the endpoint’s request model. Moreover, Wolverine can even generate slightly different code for having multiple validators versus a singular validator as a way of wringing out a little more efficiency in the common case of having only a single validator registered for the request type.

    The point here is that Wolverine is trying to generate the most efficient code possible based on what it can glean from the IoC container registrations and the signature of the HTTP endpoint or message handler methods while the MediatR model has to effectively use runtime wrappers and conditional logic at runtime.



    Read the whole story
    alvinashcraft
    just a second ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Join Microsoft at NDC London 2026 – Let’s Build the Future of .NET Together

    1 Share

    NDC London is here, with pre-conference workshops running January 26–27 and the main conference running January 28–30. If you’re a .NET or Azure developer, this is the place to be. Whether you want to sharpen your cloud-native skills, get hands-on with AI-powered development, learn from Microsoft engineers, or simply connect with people who love shipping great software, NDC London always delivers. This year, Microsoft is showing up in a bigger, more meaningful way – and we can’t wait to meet you.

    🌟 Meet the Microsoft Speakers Shaping the Future of .NET

    Expect a strong lineup of Microsoft speakers and MVPs delivering deep technical talks across .NET, AI, cloud-native development, and app modernization. Fan favorites like Chris Ayers, Damian Brady, Gerald Versluis, Steve Sanderson, and more – all delivering deeply technical sessions spanning AI-assisted development, cloud-native architecture, and modern .NET patterns. Whether you’re interested in performance tuning, agentic AI, Aspire, or real-world modernization stories, this year’s Microsoft presence gives you direct access to the people building the tools you use every day.

    🏢 Visit the Microsoft Booth – Let’s Talk Modernization, AI, and What You’re Building

    Microsoft will be on the show floor all three main conference days (January 28–30) with a staffed booth ready for real, technical conversations. Swing by Booth #11, located on the main floor of the expo space.

    Here’s why you’ll want to stop by:

    • Latest demos on the latest announcements and technologies.
    • 1:1 expert meetups with Microsoft speakers and engineers to talk through your modernization challenges, AI questions, or architectural patterns you’re exploring.
    • Swag and resources – distributed throughout the event (and yes, it will run out, so earlier is better).

    We’ll also be sharing guidance on .NET 10, GitHub Copilot, modernization tooling, Azure SRE Agent, and Managed Instance on Azure App Service – all hot topics for teams looking to move faster and modernize intelligently.

    🎯 See You in London

    Whether you’re exploring cloud-native patterns, scaling legacy .NET apps, adopting AI copilots, or just want to connect with thousands of passionate developers, we want to meet you at NDC London.

    Come find us at Booth #11 January 28–30. We can’t wait to learn what you’re building – and help you build what’s next.

    The post Join Microsoft at NDC London 2026 – Let’s Build the Future of .NET Together appeared first on .NET Blog.

    Read the whole story
    alvinashcraft
    16 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    OG Image Generation in AnalogJS

    1 Share

    AnalogJS can now generate custom OG Images in Angular on the server for dynamic image sharing.

    You may not know it, but Vercel released a package for Next.js called @vercel/og. It is unfortunately not open-source, but almost every framework has implemented their own version using Satori under the hood.

    TL;DR

    Analog now has the ability to generate OG Images in Angular on the server for SEO purposes. You can customize them and generate them on the spot. While they only work in Node environments and have limited CSS capabilities, they are extremely useful and great for dynamic image sharing.

    What Is an OG Image?

    The Open Graph Protocol is an HTML pattern that allows you to describe your webpage in the meta tags for better search engine optimization.

    <meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />
    

    When you create an image for each webpage, you allow preview images to be displayed. You can test a website with tools like Open Graph Preview.

    You can also add data with Twitter Cards and Schema.org images.

    Twitter Card

    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="Learn Latin with Interactive Flashcards">
    <meta name="twitter:description" content="Build your Latin vocabulary through smart, progressive flashcards.">
    <meta name="twitter:image" content="https://example.com/images/latin-flashcards.jpg">
    <meta name="twitter:image:alt" content="Latin flashcards with English translations">
    

    Schema.org

    Or JSON-LD for schemas:

    {
      "@context": "https://schema.org",
      "@type": "WebPage",
      "name": "How to Make French Toast",
      "url": "https://example.com/french-toast",
      "image": "https://example.com/images/french-toast.jpg",
      "description": "A simple guide to making delicious French toast.",
      "author": {
        "@type": "Person",
        "name": "Jane Doe"
      }
    }
    

    Angular Image Directive

    If you have an image on the site that can be created with CSS or Tailwind, you may also want to use it to generate dynamic images using the Angular Image Directive.

    Satori

    Vercel created a library called Satori, which converts HTML, CSS and Tailwind to an SVG image. It is a beautiful module, and it can run anywhere. However, it does not do every CSS function, nor does it convert the SVG to a PNG file, which requires rasterization. You must build a mini-canvas to convert the raw pixel data to PNG or JPG buffer. OG image parsers prefer PNG or SVG images.

    Vercel OG

    Vercel created a wrapper for this project. Unfortunately, it is not open-source and it only works on Next.js. It also uses JSX instead of pure HTML. However, there have been several implementations for it, including this one for AnalogJS.

    Installation

    First, install AnalogJS. Then install the packages.

    npm install satori satori-html sharp --save
    

    Og Image Route

    Create the file og-image.ts in /server/routes/api.

    import { defineEventHandler, getQuery } from 'h3';
    import { ImageResponse } from '@analogjs/content/og';
    
    export default defineEventHandler(async (event) => {
        const fontFile = await fetch(
            'https://cdn.jsdelivr.net/npm/@fontsource/geist-sans/files/geist-sans-latin-700-normal.woff',
        );
        const fontData: ArrayBuffer = await fontFile.arrayBuffer();
        const query = getQuery(event);
    
    const template = `
        <div tw="flex flex-col w-full h-full p-12 items-center text-center justify-center text-white bg-[${query['bgColor'] ?? '#5ce500'}]">
          <h1 tw="flex font-bold text-8xl mb-4 text-black">${query['title'] ?? 'Progress <span class="text-gray-400">Telerik</span>'}</h1>
          <p tw="flex text-5xl mb-12 text-white">${query['description'] ?? 'Kendo UI'}</p>
        </div>
    `;
    
        return new ImageResponse(template, {
            debug: true,
            fonts: [
                {
                    name: 'Geist Sans',
                    data: fontData,
                    weight: 700,
                    style: 'normal',
                },
            ],
        });
    });
    

    We can pass data from the URL parameters through getQuery. Her we are using three variables:

    • title
    • description
    • bgColor

    You can use whatever variables you want. The ImageResponse returns a png image. satori creates an svg and sharp is used to convert the image to a png.

    Dynamic Images

    Because you can pass parameters, you can create on-the-fly images through the parameters.

    <html>
      <head>
        <meta
          property="og:image"
          content="https://your-url.com/api/v1/og-images?title=Developer"
        />
        <meta
          name="twitter:image"
          content="https://your-url.com/api/v1/og-images?title=Developer"
          key="twitter:image"
        />
        ...
      </head>
    </html>
    

    Demo

    For this example, I am generating several images from the same route. The default route has no parameters.

    • /api/og-image
    • /api/og-image?title=…
    • /api/og-image?title=…&description=

    You can also pass extra fields in the title or description fields like a span.

    title=<span%20class='text-red-300'>Kendo+UI</span>
    

    Notice it is HTML encoded.

    Request

    Since we are generating the image URL first on the server, we need to get the correct URL from the request event. This is important so it can work on any server without changing the URL manually, including on the local machine.

    Edit the main.server.ts file to get the request event through provideServerContext.

    import 'zone.js/node';
    import '@angular/platform-server/init';
    import { provideServerContext } from '@analogjs/router/server';
    
    import { AppComponent } from './app/app';
    import { config } from './app/app.config.server';
    import { renderApplication } from '@angular/platform-server';
    import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
    import { ServerContext } from '@analogjs/router/tokens';
    
    export function bootstrap(context: BootstrapContext) {
      return bootstrapApplication(AppComponent, config, context);
    }
    
    export default async function render(
      url: string,
      document: string,
      serverContext: ServerContext,
    ) {
      const html = await renderApplication(bootstrap, {
        document,
        url,
        platformProviders: [provideServerContext(serverContext)],
      });
    
      return html;
    }
    

    Config

    Make sure to turn on ssr and turn off static in the vite.config.ts file.

    /// <reference types="vitest" />
    
    import { defineConfig } from 'vite';
    import analog from '@analogjs/platform';
    import tailwindcss from '@tailwindcss/vite';
    
    // https://vitejs.dev/config/
    export default defineConfig(() => ({
      build: {
        target: ['es2020'],
      },
      resolve: {
        mainFields: ['module'],
      },
      plugins: [
        analog({
          ssr: true,
          static: false,
          prerender: {
            routes: [],
          },
          nitro: {
            preset: 'vercel'
          }
        }),
        tailwindcss()
      ],
    }));
    

    ⚠️ This will NOT work in Bun, Deno, Vercel Edge Functions, nor Cloudflare. Standard Node environments and serverless functions only! Only the Next.js version will correctly compile the large WASM file necessary to convert the SVG to a PNG. Hopefully, the framework developers will get this working for those environments as well. There may be some hacks, but I have never personally succeeded to get it working.

    Origin

    Next, we need to create a token to get the correct origin URL on the server and browser. I put this in app/lib/utils.ts.

    import { injectRequest } from "@analogjs/router/tokens";
    import { isPlatformBrowser } from "@angular/common";
    import { DOCUMENT, inject, InjectionToken, PLATFORM_ID } from "@angular/core";
    
    export const ORIGIN = new InjectionToken<string>(
      'origin',
      {
        providedIn: 'root',
        factory() {
          // You could hardcode this, or just hydrate the server value
          // This shows you how to derive it dynamically
          const doc = inject(DOCUMENT);
          const platformId = inject(PLATFORM_ID);
          const isBrowser = isPlatformBrowser(platformId);
          const request = injectRequest();
          const host = request?.headers.host;
          const protocol = host?.includes('localhost') ? 'http' : 'https';
          const origin = `${protocol}://${host}`;
          return isBrowser ? doc.location.origin : origin;
        }
      }
    );
    

    referer will get the origin from the request event on the server, but we must get rid of the extra / at the end with slice. The can inject a safely usable DOCUMENT token that will not error out on the server. Now we can get the correct base URL in any environment.

    Creating the Images

    We inject the ORIGIN token to get the correct URL.

    import { Component, inject } from '@angular/core';
    import { ORIGIN } from '../lib/utils';
    import { Meta } from '@angular/platform-browser';
    
    @Component({
      selector: 'app-home',
      standalone: true,
      template: `
      <main class="flex flex-col gap-5 items-center justify-center py-10">
        <h2 class="text-6xl font-semibold">AnalogJS</h2>
        <h3 class="text-4xl font-medium">OG Image Generator</h3>
    
        <img [src]="img1" alt="Default OG Image" />
        <div class="text-6xl font-semibold">or</div>
    
        <img [src]="img2" alt="Custom Title & Description" />
        <div class="text-6xl font-semibold">or</div>
    
        <img [src]="img3" alt="Styled Title, Custom BG" />
      </main>
      `,
    })
    export default class HomeComponent {
    
      readonly origin = inject(ORIGIN);
      readonly meta = inject(Meta);
    
      constructor() {
        this.meta.updateTag({
          name: 'og:image',
          content: this.origin + '/api/og-image'
        });
      }
    
      img1 = this.origin + '/api/og-image';
    
      img2 = this.origin + '/api/og-image?title=My%20Custom%20Title&description=My%20Custom%20Description';
    
      img3 = this.origin + "/api/og-image?title=<span%20class='text-red-300'>Kendo+UI</span>&description=For%20Angular&bgColor=%23eb0249";
    }
    

    You can see our images are created dynamically from our server route /api/og-image.

    We could have hard-coded the image URLs, but then they would not work automatically if we change servers or if we test on localhost.

    Meta

    We use the Meta injection to update the meta tag dynamically.

    this.meta.updateTag({
      name: 'og:image',
      content: this.origin + '/api/og-image'
    });
    

    We could do something similar for any image URLs on the page.

    If we updated the header after a fetch or a time consuming JS function, we would need to update the tag in a resolver, or use PendingTasks to wait for the tag to be updated on the server before the server page is rendered.

    Bonus SEO Tips

    ➕ If we want to have the best SEO, we should generate our images for the best image sizes by platform.

    • og:image – 1200 x 620
    • twitter:image (use summary large) – 1600 x 900
    • JSON-LD – 1200 x 620, 1600 x 900, and 1000 x 1500 (for Pinterest)
    <head>
      <!-- Primary Meta Tags -->
      <title>Example Page Title</title>
      <meta name="description" content="Short, compelling description for SEO." />
    
      <!-- Canonical URL -->
      <link rel="canonical" href="https://example.com/your-page" />
    
      <!-- Open Graph / Facebook -->
      <meta property="og:type" content="website" />
      <meta property="og:url" content="https://example.com/your-page" />
      <meta property="og:title" content="Example Page Title" />
      <meta property="og:description" content="Short, compelling description for SEO." />
      <meta property="og:image" content="https://example.com/images/your-image-1200x630.jpg" />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="630" />
      <meta property="og:image:alt" content="Descriptive alt text for your image." />
    
      <!-- Twitter -->
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:url" content="https://example.com/your-page" />
      <meta name="twitter:title" content="Example Page Title" />
      <meta name="twitter:description" content="Short, compelling description for SEO." />
      <meta name="twitter:image" content="https://example.com/images/your-image-1600x900.jpg" />
      <meta name="twitter:image:alt" content="Descriptive alt text for your image." />
    
      <!-- JSON-LD Schema -->
      <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "WebPage",
        "name": "Example Page Title",
        "description": "Short, compelling description for SEO.",
        "url": "https://example.com/your-page",
        "image": [
          "https://example.com/images/your-image-1200x630.jpg",
          "https://example.com/images/your-image-1600x900.jpg",
          "https://example.com/images/your-image-1000x1500.jpg"
        ],
        "author": {
          "@type": "Person",
          "name": "John Doe"
        },
        "publisher": {
          "@type": "Organization",
          "name": "Example Company",
          "logo": {
            "@type": "ImageObject",
            "url": "https://example.com/images/logo-512x512.png"
          }
        }
      }
      </script>
    </head>
    

    You don’t need to specify the image sizes in JSON-LD.

    Result

    Green background with words Progress Telerik Kendo UI

    Green background with text: My Custom Title / My Custom Description

    Red background with text: Kendo UI For Angular

    Testing OG Image

    When we test our URL on Open Graph XYZ, we can see it works! Using a plain og:image tag is a minimum for most use cases.

    screenshot of images sized for Facebook and X / Twitter

    Repo: GitHub
    Demo: Vercel Functions

    Beautiful dynamic images ready to be used as your preview image.

    Read the whole story
    alvinashcraft
    22 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    6 .NET MAUI Properties You Didn’t Know Work with Buttons

    1 Share

    Learn several useful properties to help you customize and enhance your buttons in .NET MAUI, like rounding the corners buttons and enabling automatic scaling.

    Buttons are one of the most important visual elements in any application. Through them, users can perform key actions that move the app’s flow forward, like navigating to another page or calling an API. That’s why it’s essential to understand their different properties.

    There are several interesting properties that you might not even know are available for the Button in .NET MAUI, but learning about them will help you create more precise and visually appealing designs.

    In this article, you’ll discover six of those properties, understand how they work and see how to implement them. But before diving into each one, let’s take a quick preview of what’s coming.

    • TextTransform
    • FontAutoScalingEnabled
    • CharacterSpacing
    • CornerRadius
    • FontFamily
    • LineBreakMode

    1. TextTransform

    TextTransform - CLICK HERE: Uppercase, click here: Lowercase, cLICK heRe: None

    This property defines how the text inside your button will be displayed—whether it appears in uppercase, lowercase or exactly as you wrote it. This property accepts the following values:

    • Lowercase: Displays all the text in lowercase letters.
    • Uppercase: Displays all the text in uppercase letters.
    • None: Displays the text exactly as you wrote it.

    So … how do we actually use it in a button? Let’s check it out:

    <Button Text="cLICK heRe" 
      TextTransform="Lowercase" />
    

    2. FontAutoScalingEnabled

    This one’s super important! When you go to your device Settings, you can adjust how you want text to appear—larger, smaller or anywhere in between. The system shows a list of size options for you to choose from.

    With the FontAutoScalingEnabled property, .NET MAUI lets us control whether our app should follow those text-size preferences that the user has already set on their device.

    For example, you’ve probably seen people who have really large system text. If this option is enabled, your .NET MAUI apps will also adapt and display text in the same way.

    This property allows you to decide whether to enable or disable that behavior in your button:

    • If set to true, the button text adjusts to the device’s font-scaling preferences.
    • If set to false, the text stays fixed, completely ignoring the system configuration.

    All right, let’s see it in action!

    <Button Text="Hello!! " 
      FontAutoScalingEnabled="false" />
    

    But … How Do I Set Up the Preferences on My Device? ️

    To test these settings on your own device, here’s how you can make the text larger or smaller on both iOS and Android.

    • On iOS: Go to SettingsAccessibilityDisplay & Text SizeLarger Text and adjust the slider to set the text size you prefer.
    • On Android: Go to SettingsDisplayFont size and style and use the preview slider to adjust the text size.

    After following the steps above, you should see a screen similar to the one shown below:

    FontScaling settings: iOS & Android

    3. CharacterSpacing

    CharacterSpacing sample

    Defines the amount of space between each character of the button’s text and takes a value of type double. This property allows us to play with the visual rhythm of the button. You can either increase or decrease the spacing between characters depending on the style you want to achieve:

    • If you want to increase the spacing, use a positive value.
    • If you want to create a design or stylistic effect by reducing the spacing, you can use negative values.

    Let’s bring it to life inside a button!

    <Button 
      Text="CLICK HERE" 
      CharacterSpacing="5" />
    

    4. CornerRadius

    CornerRadius sample

    This property lets you define how rounded the corners of your button will be. (It takes a value of type int.) The higher the value, the more rounded the corners will appear, and the lower the value, the sharper they’ll look.

    Pro Tip: If you want to create a circular button, simply set the WidthRequest and HeightRequest properties to the same value, and assign the CornerRadius property to half of that value.

    For example:

    <Button 
      WidthRequest="80" 
      HeightRequest="80" 
      CornerRadius="40" />
    

    5. FontFamily

    With this property, you can define the font that your button will use, which helps your button maintain a consistent visual identity with the overall style of your app. By default, it uses Open Sans.

    How to use it?

    <Button 
      Text="Click here" 
      FontFamily="MontserratBold" />
    

    6. LineBreakMode

    This property is responsible for organizing how the text will be displayed on the button when it spans more than one line. It takes values from the LineBreakMode enum; depending on the value you choose, your text will be displayed in different ways. The values you can use are as follows:

    ➖ NoWrap: Regardless of the text length, it stays on a single line and only displays the number of characters that fit within the button’s width.

    ➖ WordWrap: Automatically moves the text to the next line once it reaches the button’s boundaries. Keep in mind that the line break happens after the last full word that can fit; if a word doesn’t fit completely, it is moved to the next line.

    ➖ CharacterWrap: Automatically moves the text to the next line. This may cause a word to be split if there’s only enough space for a few characters. For example, if the word “Hello” only has space for the first two letters, it will break and place the remaining letters on the next line.

    ➖ HeadTruncation: Displays the text on a single line and only shows the final part of the text.

    ➖ MiddleTruncation: Displays the beginning and the end of the text, separated by an ellipsis (…), all within a single line.

    ➖ TailTruncation: Displays only the beginning of the text and hides the rest.

    Let’s use a visual example to better understand how each one behaves:

    LineBreakMode samples: – Sample text: Click this button to open the configuration settings for your account and manage preferences. | NoWrap: Click this button to open the configuration | WordWrap: Click this button to open the configuration settings for your account and manage preferences. | CharacterWrap: Click this button to open the configuration settings for your account and manage preferences. | HeadTruncation: ...tings for your account and manage preferences. | MiddleTruncation: Click this button... and manage preferences. | TailTruncation: Click this button to open the configuration...

    Let’s make it come alive with a button example!

    <Button Text="Hello!! " 
      LineBreakMode="TailTruncation" />
    

    ✍️ Bonus Information

    There’s also another property called ContentLayout, which allows you to control the position of icons within your buttons. If you’d like to learn how to use it, I recommend checking out the article Beyond the Basics: Easy Icon Placement on .NET MAUI Buttons.

    Conclusion

    And that’s it! Now you know several useful properties that can help you customize and enhance your buttons in .NET MAUI—from TextTransform and CharacterSpacing to rounding your buttons and enabling automatic scaling.

    I hope this guide helps you discover new ways to make your buttons more dynamic and visually consistent across your apps.

    If you have any questions or would like me to dive deeper into other controls or properties, feel free to leave a comment—I’d love to help you!

    See you in the next article! ‍♀️✨

    References

    The explanation was based on the official documentation:

    Read the whole story
    alvinashcraft
    26 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    What’s the difference between Safe­Array­Access­Data and Safe­Array­Add­Ref?

    1 Share

    Once upon a time, there was SAFEARRAY, the representation of an array used by IDispatch and intended for use as a common mechanism for interchange of data between native code and scripting languages such as Visual Basic. You used the Safe­Array­Create function to create one, and a variety of other functions to get and set members of the array.

    On the native side, it was cumbersome having to use functions to access the members of an array, so there is also the Safe­Array­Access­Data function that gives you a raw pointer to the array data. This also locks the array so that the array cannot be resized while you still have the pointer, because resizing the array could result in the memory moving. The idea here is that you lock the data for access, do your bulk access, and then unlock it. As an additional safety mechanism, an array cannot be destroyed while it is locked.

    This was the state of affairs for a while, until the addition of the Safe­Array­Add­Ref function in the Window XP timeframe. I don’t know exactly the story, but from the remarks in the documentation, it appears to have been introduced to protect against malicious scripts.

    Suppose you’re writing a scripting engine, and a script performs an operation on an array. Your scripting engine represents this as a SAFEARRAY, and your engine starts operating with the array. You then issue a callback back into the script (for example, maybe you are the native side of a for_each-type function), and inside the callback, the script tries to destroy the array. After the callback returns, you have a use-after-free vulnerability in the scripting engine because it’s operating on an array that has been destroyed.

    You could update the scripting engine to perform a Safe­Array­Access­Data on the array, thereby locking it and preventing the array from being resized or destroyed while the native code is using it. But that also means that the callback won’t be able to, say, append an element to the array. The script that the callback is running would encounter a DISP_E_ARRAY­IS­LOCKED error when trying to append. If your scripting engine ignores errors from Safe­Array­ReDim, then the script’s attempt to extend the array silently fails, and that will probably break the internal script logic. As for destruction, if your script engine ignores errors from Safe­Array­Destroy, then the array will be leaked. But if your scripting engine meticulously checks for those errors, then the script will get an unexpected exception.

    For a failure to destroy a locked array, I guess the scripting engine could put the array in a queue of arrays whose destruction has been deferred, and then, I guess, check every few seconds to see if the array is safe to destroy? But for a failure to extend a locked array, the scripting engine is kind of stuck. It can’t “try again later” because the script expects the appended element to be present.

    To solve the problem while creating minimal impact upon existing code, the scripting team invented Safe­Array­Add­Ref. This is similar to Safe­Array­Access­Data in that it returns you a raw pointer to the array data, but it does not lock the array object. The array object can still be resized or destroyed successfully, thereby preserving existing semantics. What it does is add a reference to the array data (the same data that you received a pointer to). Only when the last reference is released is the data freed.

    For a resize, that means that new memory is allocated, and the values are copied across, but the old memory is not freed until a corresponding number of Safe­Array­Release­Data and Safe­Array­Release­Descriptor calls have been made. (The Add­Ref adds a reference to both the data and descriptor, and you have to release both of them in separate calls.)

    Note that even though the memory is not freed, it is nevertheless zeroed out. This avoids problems with objects that have unique ownership like BSTR. If the memory hadn’t been zeroed out, then when the array is resized, there would be two copies of the BSTR, one in the new array data, and an abandoned one in the old array data. The code that called Safe­Array­Add­Ref still has a pointer to the old array data. The new resized data might change the BSTR, which frees the old string, but the old data still has the BSTR and will result in a use-after-free.

    Next time, a brief digression, before we use this information to answer a customer question about Safe­Array­Add­Ref.

    Bonus chatter: Note however that if the code that called Safe­Array­Add­Ref writes to the old data, the any data in that memory block is not cleaned up. So don’t write a BSTR or Unknown or anything else that requires cleanup, because nobody will clean it up. (This is arguably a design flaw in Safe­Array­Add­Ref, but what’s done is done, and you have to deal with it.)

    The post What’s the difference between <CODE>Safe­Array­Access­Data</CODE> and <CODE>Safe­Array­Add­Ref</CODE>? appeared first on The Old New Thing.

    Read the whole story
    alvinashcraft
    31 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    From 5 to 50: A Practical Playbook for Scaling Your Product Team

    1 Share

    Let’s be real: scaling a product team feels less like a graceful evolution and more like trying to rebuild a plane while it’s in the air. Data shows that 70% of organizations struggle with scaling effectively, often due to unclear processes and cultural strain. Your success during this phase depends heavily on the systems you build and the team environment you maintain. Both must evolve together to avoid breakdowns.

    First, you have to get hiring and onboarding right. Research indicates that structured onboarding can improve new hire productivity by up to 50%. “Scaling-ready” talent isn’t just about impressive resumes; it’s about people who thrive in ambiguity and have a natural curiosity to solve challenging problems. Look for the storytellers and the simplifiers. Seek individuals who excel in uncertain environments and are inclined to take initiative. For example, companies like Airbnb prioritize candidates who demonstrate storytelling and simplification skills, which helps maintain clarity as the team grows.

    With new faces arriving weekly, your old ways of working will break. The daily stand-up that worked for one team becomes a 20-person monologue for three. This is where you evolve your agile practices. Maybe one squad needs strict Scrum, while another thrives on the flow of Kanban. Enforce principles, not prescriptions. And, please, embrace asynchronous communication. A well-written document in a central hub is worth a dozen scheduled meetings. The goal is to create a system where work moves forward even when people aren’t all in the same (virtual) room at the same time, and where clear escalation paths prevent decisions from getting stuck in endless debate.

    But let’s be honest, all the process in the world is useless if your best people get bored, burned out, or bail. Culture in scaling isn’t about ping-pong tables; it’s about psychological safety and clear career paths. Your job is to keep teams motivated by connecting their work to real-world impact, especially when everything feels chaotic. The single best retention tool you have is honest, constructive feedback and a visible path for growth. Show your high-performers what’s next for them, or someone else will.

    You don’t have to do everything at once. Start with one thing. A study by Atlassian found that teams using centralized documentation reduced meeting time by 25%. This week, try replacing one recurring status meeting with a shared, live document where everyone provides updates asynchronously. See what happens. You might just get an hour back—and a glimpse of a more scalable future.

    So, what’s the one thing currently blocking your team from scaling effectively? Share your biggest hurdle in the comments.

    The post From 5 to 50: A Practical Playbook for Scaling Your Product Team appeared first on Simple Thread.

    Read the whole story
    alvinashcraft
    36 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete
    Next Page of Stories