Posts tagged with “c#”

Smart Row Selection: Maintaining Infragistics UltraGrid State After Refresh

... skipping 1000 words about bad solutions ...

The Clean/Best Solution

Here's a pattern that elegantly handles this situation:

public void RefreshGrid()
{
    // 1. Store the current entity before refresh
    SomeEntity currentEntity = null;
    if (dataGrid.ActiveRow != null)
    {
        currentEntity = dataGrid.ActiveRow.ListObject as SomeEntity;
    }

    // 2. Refresh the grid
    dataGrid.RefreshData();

    // 3. Restore selection using entity ID
    if (currentEntity != null)
    {
        dataGrid.ActiveRow = dataGrid.Rows.FirstOrDefault(r => 
            ((SomeEntity)r.ListObject).Id == currentEntity.Id);
    }
}

Why This Pattern Is Best Practice

  1. Type Safety: Using the strongly-typed entity object instead of raw values
  2. Identity-Based: Uses unique IDs instead of volatile row positions
  3. Null-Safe: Handles cases where no row is selected
  4. Concise: LINQ makes the code readable and maintainable
  5. Reliable: Works even if data order changes or rows are filtered

= 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

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.

A neat way to remove trailing zeros from a decimal number in C#

We can get unexpected trailing zeros when we serialzie an object with decimal properties, normally it will not hurt. However, it does hurt when we want to compare two objects by serializing them to JSON string. I tried a few solutions and finally find the following one is the best.

public static decimal Normalize(this decimal value)
{
    return value/1.000000000000000000000000000000000m;
}

Reference

Tuple type in C#

It's a rather interesting feature. I first use it the same way as the python tuple type. I immediately found I was wrong. It doesn't support using an index to visit certain element

Stupid enough. I think. Soon I found the correct way, you know, the Item1, Item2 way.

It's so Stupid! Then I found the best way: the named element way.

Task<(List<string> orderIdList, List<string> orderNoList)> GetExpiringOrderIdListAndOrderNoList(DateTime checkTime);

Ok. It's not very stupid.