Thursday, May 27, 2021

C# 10.0: Introducing Global Usings

C# 10.0 is expected to ship with .NET 6 in November, 2021 during .NET Conf 2021 (November 9-11), but some of the features are already available with C# Preview LangVersion. One of such features is Global Usings. In this post, let's have a look at what that is.

I have created a Console Application and have set up the LangVersion to preview.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

</Project>

Now I have the following code in Program.cs file.

Course course = await GetSomeCourse();

foreach (
Student student in course.Students)
{
    
Console.WriteLine(student.Name);
}

static async 
Task<Course> GetSomeCourse()
{
    
Course course = new("C# 10"new List<Student> { new("John"), new("Jane") });

    return await 
Task.FromResult(course);
}

public record Student(string Name);

public record Course(string Name
List<Student> Students);

So here you can see I have used some C# 9 features (Top Level statements, Records,  Target-Typed new expressions etc). But here, I don't have any usings. Of course, I need to be having using SystemSystem.Collections.Generic and System.Threading.Tasks here, but where are those?

In my project, I have added another file called Usings.cs (you can name it with any name you want to).
Usings.cs
And there in the Usings.cs, I have the following.

global using System;
global using System.Collections.Generic;
global using System.Threading.Tasks;

Note the global keyword. So this means, these usings are available throughout my project. 99% of the time, there will be some usings which needs to be there in every .cs file you have, so basically, you can move all those into a different file and use it with global keyword.

Now let's say, I am adding a new class to the project.
Using appeared previously in this namespace
Compiler immediately identifies we have global usings and we don't have to repeat them here.

Isn't it nice?

Happy Coding.

Regards,
Jaliya

Wednesday, May 26, 2021

Introducing .NET Hot Reload in Visual Studio 2019 Version 16.11.0 Preview 1.0

A couple of weeks back I wrote a post about Early Support for .NET Hot Reload is Now Available with .NET 6 Preview 3, and there I went through one of the greatest features expected to go to RTM with .NET 6, which is .NET Hot Reload. There at the time of me writing the post, this feature was only available with dotnet command, and not within Visual Studio.

If you have installed the latest Preview of Visual Studio that was announced earlier today during the Microsoft Build 2021, which is Visual Studio 2019 Version 16.11.0 Preview 1.0, you might have noticed something new when you are on a debugging session in Visual Studio.
Apply Code Changes
So with Visual Studio 2019 Version 16.11.0 Preview 1.0, we now have .NET Hot Reload experience through Visual Studio (actually this was initially available with Visual Studio 2019 Version 16.10.0 Preview 2.0).

So how this is going to work is something like below. I am trying the same project that I used in my previous post, this time I am debugging through Visual Studio.
.NET Hot Reload in Visual Studio 2019 Version 16.11.0 Preview 1.0
We just need to do the code change while Debugging and then hit Apply Code Changes and that's it.

This is still in it's early stages, it's going to get improved and going to support more project types with upcoming releases. Currently, for this to work a debugging session is required, but with Visual Studio 2022, we should be able to use .NET Hot Reload without needing the debugger, this means when we do CTRL+F5 and do code changes, .NET Hot Reload feature should be patching the running application.

For more details on this feature, please go through this post:

Great things to look forward to!

Happy Coding.

Regards,
Jaliya

Monday, May 24, 2021

Azure DevOps: Granting Permission to a Pipeline to Consume an Azure Artifacts Project Scoped Feed in a Different Project

I have recently faced this npm ERR! 404 Not Found when trying to do an npm install. In this scenario, I was trying to access an Azure Artifacts Project Scoped Feed from a Pipeline that is running in a Different Project.

After some time of struggling, figured out it was a permission issue. What we need to do is, following two things.

  1. First, in the Project where the Project Scoped Feed lives in, we need to go to Project Settings -> Permissions. And then add the Pipelines Build Service to Contributors Group. Pipelines Build Service is something like "{ProjectName} Build Service ({OrganizationName})"
    Project Settings -> Permissions -> Contributors
  2. Then in Project Scoped Feed Settings, under Permissions, we need to grant Pipelines Build Service at least Collaborator access, so packages can be ingested from whatever the upstream sources the feed is setup with. If you only give read permissions, packages cannot be ingested from upstream sources.
    Feed Permission

And this should be it.

Hope that helps.

Happy Coding.

Regards,
Jaliya

Sunday, May 16, 2021

Polly: Executing an Action with Retries

In this post, let's have a look at how we can execute any Actions with Retries using Polly. It's actually quite easy.

Rather than explaining in words, a code sample would greatly explain itself.

Say I have this custom exception, so whenever I received this exception, let's say I want to add some retry logic.

public class MyCustomException : Exception
{
    public MyCustomException(string message) : base(message)
    {
    }
}

So I can create a Retry Policy and just execute whatever our action within the policy. (here I am just using the async counterpart since that what we are using almost all the time). 

using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
            {
                builder.AddConsole();
            });

            ILogger<Program> logger = loggerFactory.CreateLogger<Program>();

            AsyncRetryPolicy policy = CreatePolicyFor<MyCustomException>(logger);

            await policy.ExecuteAsync(async () =>
            {
                await DoSomething();
            });
        }

        private static async Task DoSomething()
        {
            throw new MyCustomException("Some Exception");
        }

        private static AsyncRetryPolicy CreatePolicyFor<TException>(ILogger loggerint retries = 3, int delayInSecods = 5)  where TException : Exception
        {
            return Policy
                .Handle<TException>()
                .WaitAndRetryAsync(
                    retryCount: retries,
                    sleepDurationProvider: retry => TimeSpan.FromSeconds(delayInSecods),
                    onRetry: (exceptiontimeSpanretryctx) =>
                    {
                        logger.LogWarning(exception,
                            "Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}",
                            exception.GetType().Name,
                            exception.Message,
                            retry,
                            retries);
                    }
                );
        }
    }
}

We can specify how many retries we want, the sleep duration between retries, and nicely, we can even specify an Action to be executed when retrying (like I am logging the attempt info here).

Hope this helps!

Happy Coding.

Regards,
Jaliya

Tuesday, May 11, 2021

EF Core 5.0: How to use SavePoints

In this post, let's have a look at SavePoints that was introduced with EF Core 5.0.

Savepoints are basically points within a database transaction that may later be rolled back to if an error occurs or for any other reason. Let's go by an example.

Consider the below simple MyDbContext.
public class MyDbContext : DbContext
{
    public DbSet<Category> Categories { getset; }

    public DbSet<Product> Products { getset; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"Data Source=.;Initial Catalog=EfCore5;Integrated Security=True")
            .EnableSensitiveDataLogging()
            .LogTo(Console.WriteLine, LogLevel.Information);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>(builder =>
        {
            builder
                .HasMany(x => x.Products)
                .WithOne()
                .HasForeignKey(x => x.CategoryId);
        });
    }
}

public class Category
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Product> Products { getset; }
}

public class Product
{
    public int Id { getset; }

    public string Name { getset; }

    public int CategoryId { getset; }
}
And I am inserting some data as below.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

Category category = null;
Product product = null;

using IDbContextTransaction transaction = context.Database.BeginTransaction();
try
{
    category = new Category() { Name = "Some Category" };
    context.Categories.Add(category);
    await context.SaveChangesAsync();

    await transaction.CreateSavepointAsync("CategoryCreated");

    // Setting incorrect CategoryId FK, this will throw FK constraint exception
    product = new Product { Name = "Some Product", CategoryId = 999 };
    context.Products.Add(product);
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch (Exception)
{
    await transaction.RollbackToSavepointAsync("CategoryCreated");

    // Remove the invalid existing product
    context.Products.Local.Remove(product);
    product = new Product { Name = "Some Product", CategoryId = category.Id };
    context.Products.Add(product);

    //// Update/fix the invalid existing product
    //context.Products.Local.First(x => x.Name == product.Name).CategoryId = category.Id;

    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
First I am creating a Category and then I am inserting a Product. The Product save will throw an exception because I am setting an invalid CategoryId. Then inside the catch block, I am rolling back the transaction to the SavePoint I created when the Category is created ("CategoryCreated") and retrying to insert the Product. An important thing to note here is only the transaction is getting rolled back, whatever I have added to the DbContext, is staying as it is. So we MUST either remove the invalid existing product and then re-add or update/fix the invalid existing product.

Special thanks to Dr. Holger Schwichtenberg for helping me to understand how this works when I raised an issue: https://github.com/dotnet/efcore/issues/24862.
Hope this helps.

Happy Coding.

Regards,
Jaliya