Quantcast
Channel: briancaos – Brian Pedersen's Sitecore and .NET Blog

C# Application Insights Errors are Logged as Traces – How do I log them as Exceptions?

$
0
0

So when I log to Application Insights, I first follow this guide to set up Application Insights AND File logging:

C# Log to Application Insights and File from your .NET 6 Application

To create a log statement, I can now use the ILogger interface, and log my exceptions:

public class MyClass
{
    private ILogger<MyClass> _logger;

    public MyClass(ILogger<MyClass> logger)
    {
        _logger = logger;
    }

    public void MyMethod()
    {
        try
        {
            // some code goes here
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to process message GUID: {exception}", message, ex)
        }
    }
}

The logging is this line:

_logger.LogError("some error message goes here");

But hey? My error message is logged as a Trace in Application Insights:

The exception is being logged as a Trace with the severity level "Error".
The exception is being logged as a Trace with the severity level “Error”.

SO WHAT IF I WOULD LIKE TO LOG THIS AS AN EXCEPTION?

To log errors as exceptions, add the exception as the first parameter of the call:

public void MyMethod()
{       
    try
    {
        // some code goes here
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to process message GUID: {exception}", message, ex)
    }
}

Notice the “ex” as the first parameter for LogError()? This will now log the error as an exception:

Using the exception as the first parameter will log any errors as exceptions
Using the exception as the first parameter will log any errors as exceptions

If you don’t have an exception, you can call the method with an empty exception:

// Calling with an empty exception. This will leave an empty exception line in 
// Application Insights:
_logger.LogError(new Exception(), "ExecuteAsync failed");

// So you should add a text to the exception.
// You can also use an exception of a different type
// if you wish:
_logger.LogError(new InvalidOperationException("Invalid operation"), "ExecuteAsync failed");

// You can still use custom properties in the following text:
_logger.LogError(new InvalidOperationException("Invalid operation"), "ExecuteAsync failed: {ErrorCode}", 404);

SIDE NOTE: WHAT IS THE DIFFERENCE BETWEEN AN ERROR AND AN EXCEPTION?

There are many thoughts on the difference between an error and an exception. But when it comes to Application Insights, traces with severity level “Error” will be part of your trace stack. If your log lines have a shared operation ID, you can click on any message and see all messages from the same operation ID:

The trace with severity level: Error is part of the same operation.
The trace with severity level: Error is part of the same operation.

If you click an exception, however, only the exception is shown:

When clicking exceptions, the trace stack is not shown
When clicking exceptions, the trace stack is not shown

You will need to click the tiny trace stack button to see the relation between the exception and the trace stack:

The exception is part of the trace stack, but only after clicking the tiny stack button previously
The exception is part of the trace stack, but only after clicking the tiny stack button previously

SO WHAT DO I DO? DO I USE Trace OR Exception THEN?

This is where it gets opinionated. In my opinion, you either use Traces for your entire application, or you use exceptions for your entire application. Looking for errors 2 places can be confusing. I see one exception to this rule:

Let’s say that you have an application that reads an event from a datasource like a queue or a database. If the connection to this datasource fails, you could argue that this is so severe, that the application cannot continue, and should throw an exception. If the processing of the message fails because of a business rule failing, you should throw a Trace with severity level “error”.

But it’s up to you. Anyway, you are now an Application Insights Expert. Happy coding.

MORE TO READ:


Sitecore: Unable to load the service index for source https://sitecore.myget.org/F/sc-packages/api/v3/index.json.Sitecore – The remote name could not be resolved: ‘sitecore.myget.org’

$
0
0

Here is what you do if you receive messages like this:

Errors in packages.config projects
Unable to find version ‘9.0.171219’ of package ‘Sitecore.Kernel.NoReferences’.
https://sitecore.myget.org/F/sc-packages/api/v3/index.json: Unable to load the service index for source https://sitecore.myget.org/F/sc-packages/api/v3/index.json.
An error occurred while sending the request.
The remote name could not be resolved: ‘sitecore.myget.org’

Sitecore have been hosting NuGet packages on MyGet for a long time. But Sitecore have decided to move the package storage elsewhere. The move should have been executed November 2023, but because of MyGet service have been unstable, the decision have been accelerated.

This means that you need to update your build environment:

Old UrlNew Url
https://sitecore.myget.org/F/sc-packages/api/v3/index.jsonhttps://nuget.sitecore.com/resources/v3/index.json
https://sitecore.myget.org/gallery/sc-npm-packageshttps://www.npmjs.com/

Furthermore, the artifact provider is now https://cloudsmith.io/~sitecore/repos/resources/groups/

HOW TO UPDATE NUGET

In your NuGet.config, make sure the packageSources points to nuget.sitecore.com:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <solution>
    <add key="disableSourceControlIntegration" value="true" />
  </solution>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <add key="Sitecore" value="https://nuget.sitecore.com/resources/v3/index.json" />
  </packageSources>
</configuration>

MORE TO READ:

Sitecore: Insert Link tree does not expand

$
0
0

In my development Sitecore, I had this strange situation that when I tried to insert a link, the tree would not fold out for certain items:

The content tree does not expand in the "insert link" dialog
The content tree does not expand in the “insert link” dialog

I can see that I get the following error back from the /-/item/v1/sitecore/shell?sc_itemid= call:

{“statusCode”:500,”error”:{“message”:”Template is null.”}}

With a little bit of searching, it turns out, that this branch of the content tree contains an item with no template:

This item's template have been deleted
This item’s template have been deleted

This reminiscence from a test long lost in time is visible in the content editor, but makes the “Insert Link” window fail.

To delete the item you need to use the good old DbBrowser, a tool found in Sitecore since the dawn of times. You find the tool here:

/sitecore/admin/dbbrowser.aspx

This tool is a near-database tool that allows you to do things that the shell does not. Here you can select the item in question and delete it:

DbBrowser gives you direct access to the database content
DbBrowser gives you direct access to the database content

After I deleted the item, the Insert Link works again.

MORE TO READ:

C# Dapper convert numbers to 2 decimals

$
0
0

I had this problem when inserting numbers into a SQL database using Dapper. All of my numbers did not round correctly when being inserted, even when using Math.Round() before doing the insert.

My class is something like this:

public class Product
{
    public int ID { get; set; }
    public float Price { get; set; }
    public float VolumePrice { get; set; }
    public float VolumeQuantity { get; set; }
}

And before I did the SQL insert, I made sure that the price was rounded to 2 digits:

// Pseudocode, imagine that you have a 
// source item and you update the SQL
// destination item 
var product = new Product();
product.ID = souce.ID;
product.Price = Math.Round(source.Price, 2);
product.VolumePrice = Math.Round(source.VolumePrice, 2);
product.VolumeQuantity = Math.Round(source.VolumeQuantity, 2);

But even with the Math.Round(), the SQL database inserts this:

Invalid rounded numbers when inserting floats
Invalid rounded numbers when inserting floats

It turns out, that inserting the data type float will cause the rounding issue. When I changed the data type to double, the issue went away:

public class Product
{
    public int ID { get; set; }
    public double Price { get; set; }
    public double VolumePrice { get; set; }
    public double VolumeQuantity { get; set; }
}

With the double data type, the rounding is accepted:

Using double as datatype, and dapper will recognize the rounding
Using double as datatype, and dapper will recognize the rounding

In other words: do not use float as datatype. Use double.

MORE TO READ:

C# Dapper Async Bulk Inserts

$
0
0

In order for Dapper to do bulk inserts, you need a third-party library:

Once you have this package, bulk inserts are at your fingertips. Async is supported in a rather special way. Lets see how it’s done.

First of all, this is my table:

CREATE TABLE [dbo].[Favorites](
    [rowId] [int] IDENTITY(1,1) NOT NULL,
    [userKey] [int] NOT NULL,
    [favoriteId] [nvarchar](255) NULL,
    [lastModified] [datetime] NULL,
    [isDeleted] [bit] NULL,
    [created] [datetime] NULL,
 CONSTRAINT [PK_Favorites] PRIMARY KEY CLUSTERED 
(
    [rowID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Dapper is an O/R mapper, so you’ll need a model class that represents the SQL table. The model class does not need to have the same name as the SQL table, but the fields need to be the same:

public class FavoriteDTO
{
    public int RowId { get; set; } 
    public int UserKey { get; set; } 
    public string FavoriteId { get; set; } 
    public DateTime? LastModified { get; set; } 
    public bool? IsDeleted { get; set; } 
    public DateTime? Created { get; set; } 
}

Now all you have to do is to create a repository with a bulk insert method:

using Dapper;
using Z.Dapper.Plus;
using Microsoft.Data.SqlClient;
using System.Data;

namespace MyCode
{
  public class FavoritesRepository
  {
    public FavoritesRepository()
    {
    }

    public async Task BulkInsertAsync(IEnumerable<FavoriteDTO> favorites)
    {
      using var connection = new SqlConnection("your-sql-connection-string");
      // Specifying that the "FavoriteDTO" model class should be bulk inserted
      // into the "Favorites" SQL table
      DapperPlusManager.Entity<FavoriteDTO>().Table("Favorites");        
      // Do the actual bulk insert
      await connection.BulkActionAsync(x => x.BulkInsert<FavoriteDTO>(favorites));
    }
  }
}

That’s it. You are now a Dapper expert. Happy coding.

MORE TO READ:

C# Inject ILogger into Dapper Polly Retry Policy

$
0
0

There are many articles describing how to make a retry policy when using Dapper. I especially like this extension method implementation. But you cannot inject any class into extension methods because extension methods reside in a static class. Therefore you don’t have a constructor where the injection can happen.

Instead, another easy solution is to create a base class for your Dapper repository classes.

STEP 1: THE PREREQUISITES

You need Dapper and Polly of course:

Then you need a reference to a class called “SqlServerTransientExceptionDetector“. You can either grab the code and paste in into your class, or you can reference Microsoft.EntityFrameworkCore.SqlServer NuGet:

STEP 2: THE BASE CLASS

I’m assuming you use standard dependency injection, and that you have an ILogger to be used.

using Microsoft.Extensions.Logging;
using Polly.Retry;
using Polly;
using System.ComponentModel;
using System.Data.SqlClient;
using System.Data;
using Dapper;

namespace MyCode
{
  public class DapperRetryPolicyBaseClass<T>
  {
    private readonly ILogger<T> _logger;
    private static readonly IEnumerable<TimeSpan> _retryTimes = new[]
    {
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(4),
        TimeSpan.FromSeconds(8)
    };

    private readonly AsyncRetryPolicy _asyncRetryPolicy;

    public DapperRetryPolicyBaseClass(ILogger<T> logger)
    {
      _logger = logger;

      _asyncRetryPolicy = Policy
        .Handle<SqlException>(SqlServerTransientExceptionDetector.ShouldRetryOn)
        .Or<TimeoutException>()
        .OrInner<Win32Exception>(SqlServerTransientExceptionDetector.ShouldRetryOn)
        .WaitAndRetryAsync(_retryTimes,
                        (exception, timeSpan, retryCount, context) =>
                        {
                          _logger.LogWarning(exception, "{InstanceType} SQL Exception. retry #{RetryCount}. Exception {Exception}", _logger.GetType(), retryCount, exception);
                        });
    }

    protected async Task<int> ExecuteAsyncWithRetry(IDbConnection connection, 
                                                    string sql, 
                                                    object param = null,
                                                    IDbTransaction transaction = null, 
                                                    int? commandTimeout = null,
                                                    CommandType? commandType = null
                                                   ) =>
        await _asyncRetryPolicy.ExecuteAsync(async () => await connection.ExecuteAsync(sql, param, transaction, commandTimeout, commandType));

    protected async Task<IEnumerable<T>> QueryAsyncWithRetry<T>(IDbConnection connection, 
                                                                string sql, 
                                                                object param = null,
                                                                IDbTransaction transaction = null, 
                                                                int? commandTimeout = null,
                                                                CommandType? commandType = null
                                                               ) =>
        await _asyncRetryPolicy.ExecuteAsync(async () => await connection.QueryAsync<T>(sql, param, transaction, commandTimeout, commandType));
  }
}

Being a base class, we suddenly have a constructor where the injection can happen. Also, the Polly retry policy can be defined in the constructor where the ILogger is available.

STEP 3: HOW TO USE THE BASE CLASS

using System.Data.SqlClient;
using Dapper;
using Microsoft.Extensions.Logging;

namespace MyCode
{
  public class MyRepository : DapperRetryPolicyBaseClass<MyRepository>
  {

    private readonly ILogger<JsonEventRepository> _logger;

    public JsonEventRepository(ILogger<MyRepository> logger) : base(logger)
    {
      _logger = logger;
    }

    public async Task Execute(string someDateToBeInsertedIntoATable)
    {
      using var sqlConnection = new SqlConnection("some-connection-string");
      await base.ExecuteWithRetryAsync(sqlConnection, "insert into some_table (field) values (@field)", 
        new
        {
            Field = someDateToBeInsertedIntoATable,
        }
      );
    }
  }
}

Inherit from DapperRetryPolicyBaseClass and define the class type, and the ILogger of the base class will have the same type as the MyRepository.

Then use the base retry methods. On any retry, the base class will log the retry using the ILogger.

That’s it. You are now a Dapper expert. Happy coding.

MORE TO READ:

C# Convert array of objects into a DataTable

$
0
0

This extension method lets you convert an array of objects into a DataTable. DataTables are used for SQL inserts.

This solution is not mine, in fact it has been copied so many times that I don’t know who came up with it in the first place.

STEP 1: DEFINE YOUR CLASS

The class must represent the SQL table, INCLUDING THE SORT ORDER OF THE DATABASE FIELDS. If the sort order is not correct, you will not be able to use the DataTable for SQL inserts.

This is just an example of a class. Yours will look different.

public class FavoriteDTO
{
    public int RowId { get; set; } 
    public int UserKey { get; set; } 
    public string FavoriteId { get; set; } 
    public DateTime? LastModified { get; set; } 
    public bool? IsDeleted { get; set; } 
    public DateTime? Created { get; set; } 
}

STEP 2: IMPLEMENT THIS EXTENSION METHOD

using System.ComponentModel;
using System.Data;

namespace MyCode
{
  public static class IEnumerableExtensions
  {
    public static DataTable ToDataTable<T>(this IEnumerable<T> data)
    {
      PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
      DataTable table = new DataTable();
      foreach (PropertyDescriptor prop in properties)
      {
        table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
      }
      foreach (T item in data)
      {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
          row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        table.Rows.Add(row);
      }
      return table;
    }
  }
}

STEP 3: HOW TO USE THE EXTENSION METHOD

List<FavoriteDTO> favorites = new List<FavoriteDTO>();
favorites.Add(new FavoriteDTO() { RowId = 0, UserKey = 4 });
favorites.Add(new FavoriteDTO() { RowId = 1, UserKey = 2 });
favorites.Add(new FavoriteDTO() { RowId = 2, UserKey = 0 });
favorites.Add(new FavoriteDTO() { RowId = 3, UserKey = 6 });
favorites.Add(new FavoriteDTO() { RowId = 3, UserKey = 9 });

var dataTable = favorites.ToDataTable();

That’s it. Happy coding.

MORE TO READ:

C# SqlBulkCopy insert a list of objects

$
0
0

The SqlBulkCopy class is able to bulk insert data from anything that comes from a DataTable or can be read using a SqlDataReader.

So what do you do if you have an array of objects?

You have to convert the array into a DataTable, then call SqlBulkCopy.

First of all, this is my table:

CREATE TABLE [dbo].[Favorites](
    [rowId] [int] IDENTITY(1,1) NOT NULL,
    [userKey] [int] NOT NULL,
    [favoriteId] [nvarchar](255) NULL,
    [lastModified] [datetime] NULL,
    [isDeleted] [bit] NULL,
    [created] [datetime] NULL,
 CONSTRAINT [PK_Favorites] PRIMARY KEY CLUSTERED 
(
    [rowID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

I have mapped the table into a DTO. Please notice that names AND THE ORDER of the fields in the DTO matches the names order of the fields in the SQL table:

public class FavoriteDTO
{
    public int RowId { get; set; } 
    public int UserKey { get; set; } 
    public string FavoriteId { get; set; } 
    public DateTime? LastModified { get; set; } 
    public bool? IsDeleted { get; set; } 
    public DateTime? Created { get; set; } 
}

STEP 1: CONVERT THE DTO INTO A DATATABLE

First we must convert the array of FavoriteDTO into a DataTable. This extension method will take care of that:

using System.ComponentModel;
using System.Data;
 
namespace MyCode
{
  public static class IEnumerableExtensions
  {
    public static DataTable ToDataTable<T>(this IEnumerable<T> data)
    {
      PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
      DataTable table = new DataTable();
      foreach (PropertyDescriptor prop in properties)
      {
        table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
      }
      foreach (T item in data)
      {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
          row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        table.Rows.Add(row);
      }
      return table;
    }
  }
}

STEP 2: THE BULKCOPY METHOD

This BulkInsertAsync method will call the ToDataTable() extension method before calling WriteToServerAsync, allowing us to insert the objects in bulk.

using Microsoft.Data.SqlClient;

public async Task BulkInsertAsync(IEnumerable<FavoriteDTO> favorites)
{
    try
    {
        using var connection = new SqlConnection("your connection string");
        using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
        {
            connection.Open();
            bulkCopy.DestinationTableName = "Favorites";
            await bulkCopy.WriteToServerAsync(favorites.ToDataTable());
        }
    }
    catch (SqlException e)
    {
        throw new Exception($"Failed to insert favorites: {e.Message}", e);
    }
}

That’s it. Happy coding.

MORE TO READ:


C# Connect to 2 or more databases using a Datacontext

$
0
0

This is more of a design pattern than it is a requirement. I use this technique when my application grows in size. When my application needs to connect to 2 or more databases, I use this encapsulation method to allow me to name my database connections before using them.

STEP 1: ADD 2 CONNECTION STRINGS TO THE appsettings.config

  "ConnectionStrings": {
    "Source": "************",
    "Destination": "************",
  },

STEP 2: MAKE 2 ALMOST IDENTICAL CLASSES, ONE FOR EACH CONNECTION STRING

The basic “DataContext” class looks like this:

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;

namespace MyCode
{
    public class SourceDataContext
    {
        private readonly string _connectionString;

        public SourceDataContext(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("Source");
        }

        public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
    }
}

namespace MyCode
{
    public class DestinationDataContext
    {
        private readonly string _connectionString;

        public DestinationDataContext(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("Destination");
        }

        public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
    }
}

The only difference is the connection string name. These names are only as demonstration, although they could work in a production environment. Names like “ConsumerDatacontext” or “ProductsDatacontext” are probably more common.

STEP 3: ADD THE DataContext CLASSES IN THE Program.cs

Add both classes as singletons:

services.AddSingleton<SourceDataContext>();
services.AddSingleton<DestinationDataContext>();

Now I have some good names for my database connections. The connection string is hidden, and can now be changed without having to rename multiple code files. And I can add things like Azure Managed Identity to one or more connections without interfering with calling code.

STEP 4: USE THE CLASSES IN YOUR REPOSITORIES:

This piece of pseudocode is using Dapper to select from a table. What you should be focusing on is line 17, where I create the database connection.

using Dapper;
using Microsoft.Data.SqlClient;

namespace MyCode
{
    public class MyRepository
    {
        private readonly SourceDataContext _dataContext;

        public AdvertBrandRepository(SourceDataContext dataContext)
        {
            _destinationDataContext = destinationDataContext;
        }

        public async Task<IEnumerable<MyData>> GetAll()
        {
            using var connection = _sourceDataContext.CreateConnection();
            mydata = await connection.QueryAsync<MyData>($"SELECT * from MyTable");
            return mydata;
        }
    }
}

WHAT OTHER SHENANIGANS CAN YOÚ DO WHEN YOU HAVE A Datacontext class?

You can have the connection string og one database controlled by Azure Managed Identity, and another not.

See an example on how to use Azure Managed Identity here.

You can add all available table names to the Datacontext:

namespace MyCode
{
    public class SourceDataContext
    {
        private readonly string _connectionString;

        public SourceDataContext(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("Source");
        }

        public SqlConnection CreateConnection() => new SqlConnection(_connectionString);

        public string TableName1 = "TableName1";
        public string TableName2 = "TableName2";

    }
}

// Then use the table names in the code:

using Dapper;
using Microsoft.Data.SqlClient;

namespace MyCode
{
    public class MyRepository
    {
        ...
        ...

        public async Task<IEnumerable<MyData>> GetAll()
        {
            using var connection = _sourceDataContext.CreateConnection();
            mydata = await connection.QueryAsync<MyData>($"SELECT * from {_sourceDataContext.TableName1}");
            return mydata;
        }
    }
}

Or you can have an “IsEnabled” to allow yourself to do DryRuns:

namespace MyCode
{
    public class SourceDataContext
    {
        private readonly string _connectionString;

        public SourceDataContext(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("Source");
            // In this example, there is a setting called "SourceEnabled" in the config file
            Enabled = _configuration.Get<bool>("SourceEnabled");
        }

        public SqlConnection CreateConnection() => new SqlConnection(_connectionString);
        public bool Enabled { get; private set; };
    }
}

// Then use the enabled in your code:

using Dapper;
using Microsoft.Data.SqlClient;

namespace MyCode
{
    public class MyRepository
    {
        ...
        ...

        public async Task<IEnumerable<MyData>> GetAll()
        {
            if (!_sourceDataContext.Enabled)
                return Enumerable.Empty<MyData>();
            using var connection = _sourceDataContext.CreateConnection();
            mydata = await connection.QueryAsync<MyData>($"SELECT * from {_sourceDataContext.TableName1}");
            return mydata;
        }
    }
}

MORE TO READ:

C# Game Of Life

$
0
0

The Game Of Life is a cell life simulator devised by the Cambridge mathematician John Conway.

Game Of Life in a Console Application

The simulator serves as an excellent coding example to learn new programming languages. The rules are simple. You have a grid in a predetermined size, each cell can live or die based on a set of rules:

  • If a cell is alive, and there are 0 or 1 living neighbors, the cell dies of loneliness.
  • If a cell is alive, and there are 2 or 3 living neighbors, the cell survives.
  • If a cell is alive, and there are 4 or more living neighbors, the cell dies of overpopulation.
  • If a cell is dead, and there are 3 living neighbors, the cell comes alive.

This is an example of a simple game of life implementation in C# and .net 8. The simulation uses the console output to visualize the game progress.

STEP 1: DEFINE THE GRID SIZE AND THE PRE-SEED A NUMBER OF CELLS

const int BOARD_WIDTH = 80;
const int BOARD_LENGTH = 25;
const int ACTIVE_CELLS_AT_START = 250;

// This is the game of life grid
bool[,] board= new bool[BOARD_WIDTH, BOARD_LENGTH];

// Create a number of living cells to start the simulation
var random = new Random();
for (int i = 0; i < ACTIVE_CELLS_AT_START; i++)
{
  board[random.Next(board.GetLength(0)), random.Next(board.GetLength(1))] = true;
}

First we define the game prerequisites; the size of the grid (called board) and how many cells should be alive when the simulation starts.

Then we populate the grid by randomly setting cells in the grid to true (true meaning alive, false meaning dead).

STEP 2: MAKE AN ALGORITHM TO CALCULATE THE NUMBER OF LIVING NEIGHBORS

int CountNeighbors(bool[,] board, int x, int y)
{
  int ac = 0;
  for (int i = x - 1; i <= x + 1; i++)
  {
    for (int j = y - 1; j <= y + 1; j++)
    {
      if (IsValidIndex(i, j, board.GetLength(0), board.GetLength(1)) && !IsYourself(x, y, i, j) && game[i, j])
        ac++;
    }
  }

  return ac;
}

bool IsValidIndex(int i, int j, int rows, int cols)
{
  return i >= 0 && i < rows && j >= 0 && j < cols;
}

bool IsYourself(int x, int y, int i, int j)
{
  return x == i && y == j;
}

The CountNeighBors takes the game grid and the x/y position of the cell to calculate how many of the 8 neighboring cells are alive.

The white cells are the cells to check

The algorithm needs a double loop to check the neighbors.

But it also needs to be smart enough to count even if the cell is on the edge of the grid. The IsValidIndex checks to see if the cell being investigated is within the board.

Also, we should not count the cell being investigated. The IsYourself checks to see if the cell being investigated is the current cell, and that cell is also disregarded.

STEP 4: CALCULATE THE CELL LIFE

void CalculateCellLife(bool[,] board, int x, int y)
{
  int neighbors = CountNeighbors(board, x, y);
  if (board[x, y] == true)
  {
    if (neighbors <= 1)
      board[x, y] = false;
    if (neighbors >= 4)
      board[x, y] = false;
  }
  else
  {
    if (neighbors == 3)
      board[x, y] = true;
  }
}

This method uses the CountNeighbors function to calculate the number of alive cells surrounding a specified cell, and use the game of life algorithm to determine if the cell in question should die or stay alive. As stated before, the rules are that a living cell will die if there are 0,1 or more than 4 alive surrounding cells, and some alive if there is precisely 3 live cells surrounding it.

STEP 5: THE MAIN GAME LOOP

while (true)
{
  for (int x = 0; x < board.GetLength(0); x++)
  {
    for (int y = 0; y < board.GetLength(1); y++)
    {
      CalculateCellLife(board, x, y);
      DrawCell(x, y, board[x, y]);
    }
  }
}

This is the main game loop that loops forever, takes each cell on the board, calculates if the cell must live or die, and draws the result to screen.

COMPLETE CODE:

This is the code in its completion:

const int BOARD_WIDTH = 80;
const int BOARD_LENGTH = 25;
const int ACTIVE_CELLS_AT_START = 250;

bool[,] board = new bool[BOARD_WIDTH, BOARD_LENGTH];
var random = new Random();

for (int i = 0; i < ACTIVE_CELLS_AT_START; i++)
{
  board[random.Next(board.GetLength(0)), random.Next(board.GetLength(1))] = true;
}

while (true)
{
  for (int x = 0; x < board.GetLength(0); x++)
  {
    for (int y = 0; y < board.GetLength(1); y++)
    {
      CalculateCellLife(board, x, y);
      DrawCell(x, y, board[x, y]);
    }
  }
}

void CalculateCellLife(bool[,] board, int x, int y)
{
  int neighbors = CountNeighbors(board, x, y);
  if (board[x, y] == true)
  {
    if (neighbors <= 1)
      board[x, y] = false;
    if (neighbors >= 4)
      board[x, y] = false;
  }
  else
  {
    if (neighbors == 3)
      board[x, y] = true;
  }
}

void DrawCell(int x, int y, bool isAlive)
{
  Console.SetCursorPosition(x, y);
  if (!isAlive)
  {
    Console.BackgroundColor = ConsoleColor.Black;
    Console.Write(" ");
  }
  else
  {
    Console.BackgroundColor = ConsoleColor.White;
    Console.Write(" ");
  }
}

int CountNeighbors(bool[,] board, int x, int y)
{
  int ac = 0;
  for (int i = x - 1; i <= x + 1; i++)
  {
    for (int j = y - 1; j <= y + 1; j++)
    {
      if (IsValidIndex(i, j, board.GetLength(0), board.GetLength(1)) && !IsYourself(x, y, i, j) && board[i, j])
        ac++;
    }
  }

  return ac;
}

bool IsValidIndex(int i, int j, int rows, int cols)
{
  return i >= 0 && i < rows && j >= 0 && j < cols;
}

bool IsYourself(int x, int y, int i, int j)
{
  return x == i && y == j;
}

MORE TO READ:





Latest Images