Archive of

= 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

网友语录 - 第12期

这里记录每周值得分享的网友文字,我的网摘和书摘。一般在周六发布。

愚兄 人很多懊恼来自于对他人的“怒其不争”,也就是特喜欢当爹育人。很多时候对方根本不想也无法被教育,要欣喜于这世界有花,也接纳这世界有蛆。(世间万物,各有各的活法)


书摘 《暮色将尽》

我还从没认真地使用过自己的双手,当然绣花除外,这个我很在行。想象一下,如果能用自己的双手做个书架,那该多么有益,又多么令人开心啊!我真的为此感到遗憾。

因此,总的来说,我这一生,一共有两件最主要的憾事:内心深处有一个冷酷的点,以及懒惰(缺乏行动力其实也不乏胆怯的因素,但我觉得懒惰比胆怯的比重大些)。这两件憾事真实存在,但并没有怎么太折磨我,我也没觉得该常常反思。止于此就行了吧,因天天看着不好的一面是相当无聊的事。我不觉得挖掘过去的内疚对老年人有什么意义,历史已经无法改变了。

......

看看她说的话,她说她到现在依然记得在集中营里唯一善良的纳粹邻居,在以色列感受到的令人心灵颤料的自由,以及她如何热爱英国、热爱英国人,更重要的是,她到现在依然嗜好钢琴演奏,每天都弹三小时。她曾说过,“工作是人类最棒的发明••••它让你感到快乐,因你在做事情”,她和玛丽•路易斯•莫泰希茨基一样令人惊异,而她是天生就有创造力的典型。她沉醉于生活的美妙之中,并非由于宗教的激发,“开始是这样的,我们生来就有好的一面,也有坏的一面,每个人,每一个人都是这样的。然后你会遇到激发你内心好或坏的不同境遇,我相信,这就是为什么人们要发明宗教的原因”,因此她很尊重宗教里饱含的希望,尽管她的内心未必需要宗教的支持。她身上有一种不同寻常的好运,天生就具备强有力地朝向乐观主义的本性,不论经历了怎样的际遇,她依然会这样说:“生命是美丽的,如此美丽。而一个人越老,就越能察觉到这一点。当你老了,你思考,你记忆,你关切,你明了。你因为一切而深怀感激,为一切。”她还说:“我了解所有事情坏的一面,但我只看好的一面。”

.....

我是在狗的陪伴下长大的,所以不太理解为什么有些人不喜欢狗。这种动物被人类驯养的历史很久了,与人生活在一起似乎天经地义,如虎入丛林一般自然。它们已成为人类能透彻了解其情感的唯一动物种群。它们的情感与人类何其相似,只是看起来形式简单些罢了。当一只狗焦虑、愤怒、饥饿、迷惑、快乐或充满爱意时,它将这些情绪以最纯洁的形式呈现出来,我们也能感受得到,只不过人类的这些情感早被日益增长的复杂人性扭曲变形了。狗和人类因此在简单却深刻的层面彼此相通,我多想再养一只黑色绒脸小哈巴狗,重新体验这一切啊

......

我们非常清楚生命是依照生物规律而不是个体规律运作的,个体出生、长大、生儿育女、凋零死亡让位给后来者。不管人类做着怎样的白日梦,也无法幸免这样的命运。当然,我们想要尽力延长凋零过程,以至于有时候凋零甚至比成长所经历的时间还长,因此,在这一过程中会遭遇什么,如何能尽力过好这一凋零的时光,确实值得深思。现在有这么多关于保持青春的书,还有更多有关生儿育女详尽的、实验性的经验分享,但有关凋零的记录却不多见。而我,正行走在这一凋零的路程当中,我的神经刚刚经历了小狗事件和树蕨事件,倍感痛楚,于是我问自己:“为什么我不来记录?”因此,我写了这本书。

......

不信神说明什么?缺乏想象力,还是缺乏勇气?或仅是一种遗传所赋予的性情模式?前两种情况在有神论或无神论的群体中均能发现,第三种情况只不过用家族史将这个问题回避过去罢了。虔诚但文化不高的人常觉得我的这种辩解是一种放肆,是淘气任性、拒绝自我节制的行为,但实际上不管信不信神,人们一样能勤勉地履行外界赋予我们的制约和责任,并和他人一起分享这个世界。对无神论者而言,答案可能很简单,尽管说起来有点令人难为情:他不信,只是因为他觉得自己比信神的兄弟聪明智慧。不过他那信神的兄弟从相反的角度,势必也是同样的想法,那么谁可以做中间调停人?我想我们必须接受这个事实,即关于这个问题,世界上存在着两种人。


西瓦 “家居生活的舒适与整洁无关。若非如此,则每个人都可能住在室内设计与建筑杂志刊出的那些不具生气、全无人味的房屋中。这些整理得毫无瑕疵的房间所欠缺的,或者说摄制这些房间的摄影师所刻意去除的,是经人住用的一切证据。”《家的设计史》


…more

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.

书摘: 人间相亲故事

2024-06-14 18:05:45 毕业后的恋爱里,女友有次幽怨地说,她期待的是回家时有一盏为她亮着的灯。于是,他毅然放弃了深圳的稳定工作来到广州,从零开始打拼。但爱情的甜蜜期很快过去,女友开始天天计算着工资和房价,

2024-06-13 15:37:39 他曾经奋不顾身的浪漫变成了鲁莽、冲动和毫无计划…… 注: 怎么说呢,这话又对又不对。这话有理,但谁说都不违和,可从那个人嘴里说出来就好没理。

2024-06-13 15:39:37 他原本“希望与对方有话题可以聊,价值观至少要匹配”,但一年下来却发现,“很难找到能好好说话的人”。

2024-06-13 20:09:38 小林是他的初恋女友,也是他的情感启蒙老师。29岁的陆昱辰,像新生儿颤巍巍地迈出了第一步——跨进了一个没有爸妈、只有他和小林的平行空间。他们一起度过了将近一年的时光,逛遍了彼此都很熟悉的北京、看遍了新上线的电影,还去丽江等地旅行。他们也吵架,又和好。他们谈论朋友,讨论工作,唯独不谈论未来。

2024-06-14 08:35:31 我远远地看着,看东子和一个刚刚认识两天的姑娘结婚。我远远地看着,看东子和一个刚刚认识两天的姑娘结婚。

2024-06-14 08:43:00 我远远地看着。看东子和一个刚刚认识两天的姑娘,并排站在装饰得极浪漫温馨的礼台中央,表白、宣誓、拥抱、互换婚戒,相视而立,亲友哄笑着把两个人的脸颊贴在一起,他们也各自捆绑住了彼此始料未及的今生。