Execute SqlServer scripts at startup – Aspire 9.1 vs Aspire 9.2

One of the usual things in Software development is creating the database with all the tables,views, stored procedures and anything related . For Aspire 9.1 with Sql Ser, this involved a coordination between some shell scripts – see the code :

var sqlserver = builder.AddSqlServer("sqlserver", paramPass, 1433)
    .WithDbGate()
// Mount the init scripts directory into the container.
.WithBindMount("./sqlserverconfig", "/usr/config")
// Mount the SQL scripts directory into the container so that the init scripts run.
.WithBindMount("../../Scripts/data/sqlserver", "/docker-entrypoint-initdb.d")
// Run the custom entrypoint script on startup.
.WithEntrypoint("/usr/config/entrypoint.sh")

The scripts where in the mount binding. Also, some .gitattributes files was required with

* text=auto
*.sh text eol=lf
*.mod text eol=lf
*.sum text eol=lf

to can execute the .sh entry point. An painfull orchestration just for create tables

For Aspire 9.2 this has been simplified a bit , because of Creation Script for database

var db = sqlserver.AddDatabase("MyDB")
    .WithCreationScript("....")

However , this is has some flaws :

1 .Executing in the timeout of an sql

2, Executing just the last one

You can find the code at SqlServerBuilderExtensions.cs :

 private static async Task CreateDatabaseAsync(SqlConnection sqlConnection, SqlServerDatabaseResource sqlDatabase, IServiceProvider serviceProvider, CancellationToken ct)
 {
     try
     {
         var scriptAnnotation = sqlDatabase.Annotations.OfType<SqlServerCreateDatabaseScriptAnnotation>().LastOrDefault();

         if (scriptAnnotation?.Script == null)
         {
             var quotedDatabaseIdentifier = new SqlCommandBuilder().QuoteIdentifier(sqlDatabase.DatabaseName);
             using var command = sqlConnection.CreateCommand();
             command.CommandText = $"IF ( NOT EXISTS ( SELECT 1 FROM sys.databases WHERE name = @DatabaseName ) ) CREATE DATABASE {quotedDatabaseIdentifier};";
             command.Parameters.Add(new SqlParameter("@DatabaseName", sqlDatabase.DatabaseName));
             await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
         }
         else
         {
             using var reader = new StringReader(scriptAnnotation.Script);
             var batchBuilder = new StringBuilder();

             while (reader.ReadLine() is { } line)
             {
                 var matchGo = GoStatements().Match(line);

                 if (matchGo.Success)
                 {
                     // Execute the current batch
                     var count = matchGo.Groups["repeat"].Success ? int.Parse(matchGo.Groups["repeat"].Value, CultureInfo.InvariantCulture) : 1;
                     var batch = batchBuilder.ToString();

                     for (var i = 0; i < count; i++)
                     {
                         using var command = sqlConnection.CreateCommand();
                         command.CommandText = batch;
                         await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
   

So I made my own extension,

  [GeneratedRegex(@"^\s*GO(?<repeat>\s+\d{1,6})?(\s*\-{2,}.*)?\s*$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
  internal static partial Regex GoStatements();
  public static IResourceBuilder<SqlServerDatabaseResource> ExecuteSqlServerScripts(this IResourceBuilder<SqlServerDatabaseResource> db, params IEnumerable<string> sqlScripts)
  {
      var builder = db.ApplicationBuilder;
      builder.Eventing.Subscribe<ResourceReadyEvent>(async (ev, ct) =>
      {
          if (ev.Resource is not SqlServerDatabaseResource dbRes) return;
          if (db.Resource != dbRes) return;
          var cn = await dbRes.ConnectionStringExpression.GetValueAsync(ct);
          if (cn == null) return;
          using var sqlConnection = new SqlConnection();
          sqlConnection.ConnectionString = cn;
          await sqlConnection.OpenAsync(ct);
          foreach (var item in sqlScripts)
          {
              using var reader = new StringReader(item);
              var batchBuilder = new StringBuilder();

              while (reader.ReadLine() is { } line)
              {
                  var matchGo = GoStatements().Match(line);

                  if (matchGo.Success)
                  {
                      // Execute the current batch
                      var count = matchGo.Groups["repeat"].Success ? int.Parse(matchGo.Groups["repeat"].Value, CultureInfo.InvariantCulture) : 1;
                      var batch = batchBuilder.ToString();

                      for (var i = 0; i < count; i++)
                      {
                          using var command = sqlConnection.CreateCommand();
                          command.CommandText = batch;
                          //TODO: modify the timeout
                          command.CommandTimeout = 120;
                          try
                          {
                              await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
                          }
                          catch (Exception ex)
                          {
                              //TODO: log
                              Console.WriteLine($"!!!!Error in executing {batch} : {ex.Message}");
                          }
                      }

                      batchBuilder.Clear();
                  }
                  else
                  {
                      // Prevent batches with only whitespace
                      if (!string.IsNullOrWhiteSpace(line))
                      {
                          batchBuilder.AppendLine(line);
                      }
                  }
              }

              // Process the remaining batch lines
              if (batchBuilder.Length > 0)
              {
                  using var command = sqlConnection.CreateCommand();
                  var batch = batchBuilder.ToString();
                  command.CommandText = batch;
                  //TODO: modify the timeout
                  try
                  {
                      await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
                  }
                  catch (Exception ex)
                  {
                      //TODO: log

                      Console.WriteLine($"!!!!Error in executing {batch} : {ex.Message}");
                  }
              }
          }
      });
      return db;
  }

And can be executed like this:

var db = sqlserver.AddDatabase("MyDB")
    .WithSqlPadViewerForDB(sqlserver)
    .ExecuteSqlServerScripts(DBFiles.FilesToCreate)

NetCoreUsefullEndpoints-part 14–adding roles and claims and authorization

I have added the current user role and claims to the nuget Usefull Endpoints for .NET Core  .  The endpoints are

api/usefull/user/isInRole/{roleName}

and

api/usefull/user/claims/simple

and

api/usefull/user/authorization

For the first one the code is pretty simple

 route.MapGet("api/usefull/user/isInRole/{roleName}", (HttpContext httpContext, string roleName) =>
        {            
            return httpContext.User?.IsInRole(roleName)??false;

        }).WithTags("NetCoreUsefullEndpoints")
        .WithOpenApi();

For claims and authorization , please see code at ignatandrei/NetCoreUsefullEndpoints: Usefull Endpoints for .NET Core

Aspire 9.x add sql server database viewer

.NET Aspire is a formidable tool to visualize your components and relation between them . Today I will show you how to add a custom visualizer for SqlServer database .

The code for adding a database is pretty simple

var paramPass = builder.AddParameter("password", "P@ssw0rd");
var sqlserver = builder.AddSqlServer("sqlserver", paramPass, 1433)
    .WithDbGate()
;
var db = sqlserver.AddDatabase("NewDB")
;

 

 

The community extension, https://github.com/CommunityToolkit/Aspire, has already an extension, WithDBGate, that adds a viewer for the whole SqlServer . But I want something faster, that adds just for the database . So SqlPad , https://getsqlpad.com/ , that has also a docker container , will be enough. So this is the code

public  static class SqlServerExtensions
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="db"></param>
    /// <param name="sqlserver"></param>
    /// <returns></returns>
    public static IResourceBuilder<SqlServerDatabaseResource> WithSqlPadViewerForDB(this IResourceBuilder<SqlServerDatabaseResource> db,IResourceBuilder<SqlServerServerResource> sqlserver) 
    {
    var builder = db.ApplicationBuilder;
    
    var sqlpad = builder
.AddContainer("sqlpad", "sqlpad/sqlpad:latest")
.WithEndpoint(5600, 3000, "http")
.WithEnvironment("SQLPAD_AUTH_DISABLED", "true")
.WithEnvironment("SQLPAD_AUTH_DISABLED_DEFAULT_ROLE","Admin")
.WithEnvironment("SQLPAD_ADMIN", "admin@sqlpad.com")

.WithEnvironment("SQLPAD_ADMIN_PASSWORD", "admin")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__name", sqlserver.Resource.Name)
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__driver", "sqlserver")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__host", sqlserver.Resource.Name)
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__database", db.Resource.Name)
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__username", "sa")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo__password", sqlserver.Resource.PasswordParameter.Value)


.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__name", "SqlMaster")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__driver", "sqlserver")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__host", sqlserver.Resource.Name)
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__database", "master")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__username", "sa")
.WithEnvironment("SQLPAD_CONNECTIONS__sqlserverdemo1__password", sqlserver.Resource.PasswordParameter.Value)
.WithParentRelationship(db)
.WaitFor(db)
.WaitFor(sqlserver)
;
    return db;
}
}

And the code for adding is

  var db = sqlserver.AddDatabase("NewDB")
  .WithSqlPadViewerForDB(sqlserver);

Demeter and Roslyn–part 3–config and tests


Beyond the Squiggles: Flexible Reporting and Confidence Through Testing

So, we’ve built our Law of Demeter analyzer, RSCG_Demeter, using Roslyn to intelligently spot those overly-chatty code chains, even navigating the complexities of fluent interfaces. We could just have it throw warnings or errors directly in your IDE via Roslyn diagnostics.

But let’s be honest, sometimes a flood of new diagnostics can feel a bit… harsh, especially when introducing a new rule to a large codebase. Maybe you want a summary report instead, something you can review offline or integrate into a CI process?

Flexibility First: Sending Demeter Violations to a File

I wanted to offer more control. What if you could just get a list of potential violations written to a file? Good news – you can!

RSCG_Demeter allows you to configure file output directly in your .csproj. Just add these settings:

<!-- In your .csproj file -->
<PropertyGroup>
  <!-- Make the build property visible to the analyzer -->
  <CompilerVisibleProperty Include="RSCG_Demeter_GenerateFile" />
</PropertyGroup>

<PropertyGroup>
  <!-- Define the output file path (relative to the csproj) -->
  <!-- Example: Output to 'DemeterReport.txt' in the project's parent directory -->
  <RSCG_Demeter_GenerateFile>../DemeterReport.txt</RSCG_Demeter_GenerateFile>
</PropertyGroup>

With this configuration, instead of diagnostics, the analyzer will neatly output all identified potential violations to the specified file.

How does it work under the hood? The analyzer uses Roslyn’s AnalyzerConfigOptionsProvider to read this custom MSBuild property. It even cleverly grabs the project directory (build_property.ProjectDir) to correctly resolve relative paths:

// Simplified snippet showing how the analyzer reads the setting
var writeToFile = context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
{
    // Try reading the custom build property
    provider.GlobalOptions.TryGetValue("build_property.RSCG_Demeter_GenerateFile", out var filePath);

    if (!string.IsNullOrWhiteSpace(filePath))
    {
        // Handle relative paths using the ProjectDir property
#pragma warning disable RS1035 // Analyzer correctness rule
        if (!Path.IsPathRooted(filePath) &amp;&amp;
            provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out var csproj) &amp;&amp;
            !string.IsNullOrWhiteSpace(csproj))
        {
            filePath = Path.GetFullPath(Path.Combine(csproj, filePath));
        }
#pragma warning restore RS1035
        return filePath; // Return the absolute path
    }
    return string.Empty; // No file path configured
});

// Later, the analyzer checks if 'writeToFile' has a value
// and writes the report instead of creating diagnostics.

This gives you the flexibility to choose how you want to consume the Demeter analysis results – immediate feedback via diagnostics or a consolidated report via file output.

Building Confidence: It’s Tested!

An analyzer is only useful if it’s reliable. How do we know RSCG_Demeter correctly identifies violations and doesn’t flag valid patterns like our fluent builders? Testing!

The repository includes a dedicated test project (RSCGDemeter_Tests) packed with unit tests covering various scenarios, including:

  • Simple violations

  • Fluent interface patterns (return this)

  • Complex LINQ chains

  • Edge cases

These tests ensure the logic we discussed – from the initial dot counting to the semantic analysis of return types – works as expected.

Ready to Give it a Go?

Whether you prefer direct IDE feedback or a generated report, RSCG_Demeter aims to provide a robust and flexible way to encourage adherence to the Law of Demeter in your C# projects.

Check out the code, explore the tests, and try it on your solution!

➡️ https://github.com/ignatandrei/RSCG_Demeter

Let’s write less coupled, more maintainable C# together!


Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.