Posts in category “Programming”

Implementing QuickFix/n Logging with NLog

Wen working with QuickFix/n ibrary , efficient logging is crucial for troubleshooting. Here's how to implement a custom logging solution that routes QuickFix logs through NLog to your ELK stack.

Key Components

  1. NLogAdapter: A custom adapter that implements QuickFix's ILog interface:
public class NLogAdapter(ILogger logger) : ILog
{
    private const string HeartbeatPattern = @"\x0135=0\x01";
    private static readonly Regex HeartbeatRegex = new(HeartbeatPattern, RegexOptions.Compiled);

    private static bool IsHeartBeat(string message) => HeartbeatRegex.IsMatch(message);

    public void OnIncoming(string message)
    {
        if (!IsHeartBeat(message))
        {
            logger.Info("Incoming: {Message}", message);
        }
    }
    // ... other implementations
}
  1. NLogQuickFixLogFactory: A factory class to create log instances:
public class NLogQuickFixLogFactory(ILog logger) : ILogFactory
{
    public ILog Create(SessionID sessionId) => logger;
    public ILog CreateNonSessionLog() => logger;
}

Implementation Steps

  1. Register Dependencies in your DI container:
builder.Services.AddSingleton<NLog.ILogger>(_ => LogManager.GetCurrentClassLogger());
builder.Services.AddSingleton<ILog, NLogAdapter>();
builder.Services.AddSingleton<ILogFactory, NLogQuickFixLogFactory>();
  1. Configure QuickFix to use the custom logger:
var initiator = new SocketInitiator(
    clientApp,
    storeFactory,
    sessionSettings,
    new NLogQuickFixLogFactory(quickfixLogger)  // Use custom logger injeted by ILog here 
);

Key Features

  • Heartbeat Filtering: Reduces log noise by filtering out FIX heartbeat messages
  • Structured Logging: Uses NLog's structured logging format for better parsing in ELK
  • Separation of Concerns: Cleanly separates QuickFix logging from application logging

Benefits

  1. Centralized logging in ELK stack
  2. Better debugging apabilities
  3. Reduced log volume through heartbeat filtering
  4. Consistent logging format across your application

Common NUnit `Assert` statements

Here’s a consolidated list of common NUnit Assert statements, categorized by their purpose. This should cover most of the common scenarios:


Basic Assertions

  • Equality:

    Assert.That(actual, Is.EqualTo(expected));
    Assert.That(actual, Is.Not.EqualTo(expected));
    
  • Boolean Conditions:

    Assert.That(condition, Is.True);
    Assert.That(condition, Is.False);
    
  • Null Checks:

    Assert.That(obj, Is.Null);
    Assert.That(obj, Is.Not.Null);
    

String Assertions

  • Contains:

    Assert.That(actualString, Does.Contain(substring));
    
  • Starts With / Ends With:

    Assert.That(actualString, Does.StartWith(prefix));
    Assert.That(actualString, Does.EndWith(suffix));
    
  • Empty or Not Empty:

    Assert.That(actualString, Is.Empty);
    Assert.That(actualString, Is.Not.Empty);
    
  • Matches Regex:

    Assert.That(actualString, Does.Match(regexPattern));
    

Collection Assertions

  • Contains Item:

    Assert.That(collection, Does.Contain(item));
    
  • Has Specific Count:

    Assert.That(collection, Has.Count.EqualTo(expectedCount));
    
  • Empty or Not Empty:

    Assert.That(collection, Is.Empty);
    Assert.That(collection, Is.Not.Empty);
    
  • Unique Items:

    Assert.That(collection, Is.Unique);
    

Numeric Assertions

  • Greater Than / Less Than:

    Assert.That(actual, Is.GreaterThan(expected));
    Assert.That(actual, Is.LessThan(expected));
    
  • Greater Than or Equal / Less Than or Equal:

    Assert.That(actual, Is.GreaterThanOrEqualTo(expected));
    Assert.That(actual, Is.LessThanOrEqualTo(expected));
    
  • In Range:

    Assert.That(actual, Is.InRange(lower, upper));
    

Type Assertions

  • Instance of Type:

    Assert.That(obj, Is.TypeOf<ExpectedType>());
    Assert.That(obj, Is.InstanceOf<ExpectedType>());
    
  • Assignable From:

    Assert.That(obj, Is.AssignableTo<ExpectedType>());
    

Exception Assertions

  • Throws Exception:

    Assert.Throws<ExpectedExceptionType>(() => { methodCall(); });
    
  • Throws Specific Exception with Condition:

    var ex = Assert.Throws<ExpectedExceptionType>(() => { methodCall(); });
    Assert.That(ex.Message, Does.Contain("expected message"));
    

Miscellaneous

  • Same Instance:

    Assert.That(actual, Is.SameAs(expected));
    Assert.That(actual, Is.Not.SameAs(expected));
    
  • Applies a Condition:

    Assert.That(collection, Has.Some.Matches<ExpectedType>(item => item.Condition));
    
  • Delayed Constraints (Asynchronous):

    Assert.That(() => condition, Is.True.After(500).PollEvery(50));
    
  • Group related assertions together to improve readability and reporting:

    Assert.Multiple(() =>
    {
        Assert.That(okResult, Is.Not.Null, "okResult should not be null");
        Assert.That(okResult.Value, Is.TypeOf<string>(), "Value should be of type string");
    });
    

= null! vs. = string.Empty: Best Practices for Model Class Initialization

The choice between = null! and = string.Empty depends on the context and the intent of the code. Here's a breakdown to help you decide:


= null!

When to Use:

  1. You Intend to Initialize the Field Later:

    • Use = null! when the property or field is expected to be set by a constructor, a method, or dependency injection after the object is created.

    • Example:

      public string Name { get; set; } = null!;
      
    • Why: null! tells the compiler to trust you that the value will be assigned before being accessed.

  2. Avoid Premature Default Values:

    • If a default value like string.Empty doesn't make sense for the context, null! keeps the intent clearer.
  3. For Models or Data Contracts:

    • In cases where fields are expected to match a schema or be deserialized from external sources, it might be better to indicate "unknown until set."

    • Example:

      public string Username { get; set; } = null!; // Will be deserialized later
      

= string.Empty

When to Use:

  1. Default Values Are Meaningful:

    • Use = string.Empty when an empty string is a valid and reasonable default.

    • Example:

      public string Name { get; set; } = string.Empty;
      
    • Why: This avoids null-checking or potential runtime errors when accessing the property.

  2. Simpler to Work With:

    • If the field/property is commonly accessed and null is not a meaningful value in your application, an empty string simplifies the logic (avoids extra null-checks).
  3. UI or Display-Friendly Fields:

    • For fields used in UI contexts, empty strings often make more sense than null:

      public string Description { get; set; } = string.Empty;
      

Key Differences

Aspect = null! = string.Empty
Purpose Indicates value will be set later. Provides an immediate, valid default.
Compiler Warnings Suppresses nullability warnings (unsafe). Avoids nullability warnings entirely.
Clarity Explicitly states "not initialized yet". Implies "initialized to empty value now".
Best Fit Models, external contracts, DI patterns. Readily usable properties or fields.

When to Avoid = null!

  • When it's unclear who or what will initialize the property.
  • When using null might lead to accidental runtime errors.
  • When the property will be frequently accessed before initialization.

Recommendation

  • Use = string.Empty when empty strings make sense as defaults and simplify code.
  • Use = null! when initialization will occur later, and null isn't a valid or meaningful runtime value.

You know who is the real author of this article, don't you? :P

Migrated your moments from HappyFeed to HappyNotes

First, get your moments data

(function () {
  const MAX_WAIT_TIME = 50000; // If the button is always not there, we wait at most 50 seconds.
  const CHECK_INTERVAL = 1000; // If the button is not there, we do the same check after 1 scond
  const buttonSelector = '#__next > main > div > div.MomentHistory_moment-history__inner__a9etG > button';

  // Initially finding the button
  const button = document.querySelector(buttonSelector);
  if (button) {
    button.click();
    console.log('Found the button and clicked');
  } else {
    console.log('No Load button at all');
    return
  }

  // 当前已等待的时间
  let elapsedTime = 0;

  // 定时器开始循环查找,立即获得 intervalId
  const intervalId = setInterval(() => {
    elapsedTime += CHECK_INTERVAL;

    // Searching the Load button
    const retryButton = document.querySelector(buttonSelector);
    if (retryButton) {
      retryButton.click();
      elapsedTime = 0
      console.log('Found the button and clicked');
    } else {
      console.log('Could not find the button, continue searching...');
    }

    // If we could no longer find the button in MAX_WAIT_TIME, we know the work has been done.
    if (elapsedTime >= MAX_WAIT_TIME) {
      console.error('Could not find the button any more, stopping....');
      clearInterval(intervalId); // Stop the interval
    }
  }, CHECK_INTERVAL); // check if we can find the button every second
})()

Second, extract the data into a JSON object from the long page we get by clicking the Load button again and again by the script above

const diaryEntries = [];
const listItems = document.querySelectorAll('.MomentListItem_moment-list__item__d5UJL');

// Helper function:Convert date into yyyy-MM-dd format
function formatDate(dateString) {
    const months = {
        January: "01", February: "02", March: "03", April: "04", May: "05", June: "06",
        July: "07", August: "08", September: "09", October: "10", November: "11", December: "12"
    };
    const [month, day, year] = dateString.split(" ");
    return `${year}-${months[month]}-${day.padStart(2, '0')}`;
}

listItems.forEach(item => {
    const dateElement = item.querySelector('.MomentListItem_moment-item__date__XfMA8');
    const contentElement = item.querySelector('.MomentListItem_moment-item__text__F5V2C > p');
    const photoElement = item.querySelector('.MomentImage_root__uLWy7 img');
    const videoElement = item.querySelector('.MomentListItem_moment-item__video__16FUa video');

    const entry = {
        pdate: dateElement ? formatDate(dateElement.textContent.replace('Posted ', '').trim()) : null,
        content: contentElement ? contentElement.textContent.trim() : '',
        photo: ''
    };

    if (photoElement) {
        entry.photo = photoElement.getAttribute('src');
    } else if (videoElement) {
        const thumbnail = videoElement.getAttribute('poster');
        entry.photo = thumbnail ? thumbnail.trim() : '';
    }

    diaryEntries.push(entry);
    diaryEntries.reverse(); // make sure the old data is at the beginning
});

Third, send the moments data to HappyNotes by the following script

// Your personal token
const token = "eyJh...";

// API endpoint
const apiUrl = "https://happynotes-api.shukebeta.com/note/post";

// Function to send a note
async function sendNote(note) {
    const contentWithPhoto = note.photo 
        ? `${note.content}\n\n ![image](${note.photo})`
        : note.content;

    const payload = {
        isprivate: true,
        isMarkdown: true,
        publishDateTime: note.pdate,
        timezoneId: "Pacific/Auckland",
        content: contentWithPhoto
    };

    try {
        const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${token}`,
                "Content-Type": "application/json"
            },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        console.log(`Note sent successfully: ${await response.text()}`);
    } catch (error) {
        console.error(`Error sending note: ${error.message}`);
    }
}

// Send notes one by one
(async () => {
    var date = ''
    var minutes = '00'
    for (const note of diaryEntries) {
        if (note.pdate != date) {
           date = note.pdate
           minutes = '00'
        } else {
           minutes = (+minutes + 10) + ''
        }
        note.pdate += ` 20:${minutes}:00`
        await sendNote(note);
    }
})();

You need to get the token by a manually login. Don't share the token to anyone else! Anyone who gets the token can access your account and data. Follow the following steps to get your personal token.

Open the DevTools on Chrome or Firefox, then do a fresh login.

image

Solving AutoMapper Errors When Mapping Fields That Don't Exist in the Target Class

When working with AutoMapper, you may encounter errors when trying to map properties from a source object to a target object, especially if the source contains properties that don't exist in the target. This is a common pitfall, but it’s easy to resolve once you understand the root cause.

The Issue:

Consider the scenario where we have a PostNoteRequest class containing two new fields: PublishDateTime and TimezoneId. These fields are necessary for calculating the CreatedAt property of the target Note class, but the Note class doesn’t have PublishDateTime or TimezoneId at all.

Here’s a typical AutoMapper mapping that results in an error:

CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, _ => _.MapFrom((src,dst) =>
    {
        // Logic to calculate CreatedAt based on PublishDateTime and TimezoneId
    }));

Even though we're calculating CreatedAt based on the source properties, AutoMapper will try to map PublishDateTime and TimezoneId directly from the source to the target, resulting in an error like this:

Error mapping types.
Mapping types:
PostNoteRequest -> Note
HappyNotes.Models.PostNoteRequest -> HappyNotes.Entities.Note

The issue arises because AutoMapper expects all properties mentioned in the MapFrom expression to exist in both the source and target objects.

The Cause:

AutoMapper doesn't know how to handle fields (PublishDateTime and TimezoneId) that don’t exist in the target class (Note). The moment we reference these fields directly in the delegate passed to MapFrom, AutoMapper assumes they need to be mapped.

The Solution:

To solve this, we have two main approaches:

1. Use a Custom Value Resolver

A custom value resolver allows us to decouple the logic of calculating CreatedAt from the mapping process. It ensures that we don't reference non-existent fields in the target object.

public class CreatedAtResolver : IValueResolver<PostNoteRequest, Note, long>
{
    public long Resolve(PostNoteRequest source, Note destination, long member, ResolutionContext context)
    {
        if (!string.IsNullOrWhiteSpace(source.PublishDateTime) && !string.IsNullOrWhiteSpace(source.TimezoneId))
        {
            return DateTime.UtcNow.ToUnixTimeSeconds();
        }
        
        var dateStr = source.PublishDateTime;
        if (dateStr.Length == 10) dateStr += " 20:00:00";

        DateTime date = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(source.TimezoneId);

        return TimeZoneInfo.ConvertTime(new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, DateTimeKind.Unspecified), timeZone).ToUnixTimeSeconds();
    }
}

// In your mapping configuration
CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, opt => opt.MapFrom<CreatedAtResolver>());

This approach encapsulates the logic for CreatedAt calculation in a resolver, which avoids directly referencing PublishDateTime or TimezoneId in the target object.

2. Use ConstructUsing for Manual Construction

Another way to handle this is by manually constructing the target object in the ConstructUsing method, ensuring CreatedAt is set correctly without relying on AutoMapper’s default property mapping behavior.

CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, opt => opt.MapFrom((src, dst) =>
    {
        var dateStr = src.PublishDateTime;
        if (dateStr.Length == 10) dateStr += " 20:00:00";
        
        DateTime date = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(src.TimezoneId);
        
        return TimeZoneInfo.ConvertTime(new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, DateTimeKind.Unspecified), timeZone).ToUnixTimeSeconds();
    }))
    .AfterMap((src, dst) =>
    {
        dst.Tags = string.Join(" ", dst.TagList);
        dst.Content = dst.IsLong ? src.Content.GetShort() : src.Content;
    });

This method allows you to explicitly handle the CreatedAt logic while avoiding AutoMapper’s attempt to map non-existent properties from the source object.

Conclusion:

AutoMapper is a powerful tool, but it expects the properties referenced in its mapping expressions to exist in both the source and target objects. When you need to perform custom logic (like calculating CreatedAt based on other properties), using custom value resolvers or manual construction can help you avoid errors and maintain clear and maintainable code.

By understanding how AutoMapper expects the mapping to work, you can prevent issues related to missing properties and ensure your mappings are both efficient and correct.