Once time fetch API when render many of the same component

I have a Blazor component that needs to get data via the API, the problem is I call fetch API in OnInitializedAsync function, so every time I render the component the fetch API will call.

Because the results of data are the same in these components, so I want to call API once time and cache in session storage to reuse

My idea is to implement SemaphoreSlim service and the others call to it should be waiting until it is released, and I can reuse the data from storage. I don’t know if this is a bad or good idea. Could you please share your opinion and some other ideas to do this? Thank you for your response.

This is my code

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// Razor code bind
public class DateEdit: ComponentBase
{
[Inject] ICalendDataListService calendDataListService { get; set; }
List<Calend> listData;
protected override async Task OnInitializedAsync()
{
listData = await calendDataListService.GetDataByDays(
new DateOnly(DateTime.Now.Year, 1, 1).ToString("yyyyMMdd"),
new DateOnly(DateTime.Now.Year, 12, 31).ToString("yyyyMMdd"));
}
}
</code>
<code>// Razor code bind public class DateEdit: ComponentBase { [Inject] ICalendDataListService calendDataListService { get; set; } List<Calend> listData; protected override async Task OnInitializedAsync() { listData = await calendDataListService.GetDataByDays( new DateOnly(DateTime.Now.Year, 1, 1).ToString("yyyyMMdd"), new DateOnly(DateTime.Now.Year, 12, 31).ToString("yyyyMMdd")); } } </code>
// Razor code bind
public class DateEdit: ComponentBase
{
    [Inject] ICalendDataListService calendDataListService { get; set; }
    List<Calend> listData;
    protected override async Task OnInitializedAsync()
    {
        listData = await calendDataListService.GetDataByDays(
            new DateOnly(DateTime.Now.Year, 1, 1).ToString("yyyyMMdd"),
            new DateOnly(DateTime.Now.Year, 12, 31).ToString("yyyyMMdd"));
    }
}

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// Service
public interface ICalendDataListService
{
Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default);
}
public class CalendDataListService : ICalendDataListService
{
private IJSInProcessRuntime _jSInProcessRuntime;
private static readonly SemaphoreSlim _semaphore = new(initialCount: 1, 1);
private NavigationManager navigationManager;
public CalendDataListService(IJSInProcessRuntime jSInProcessRuntime, NavigationManager navigationManager)
{
_jSInProcessRuntime = jSInProcessRuntime;
this.navigationManager = navigationManager;
}
public async Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default)
{
try
{
await _semaphore.WaitAsync(token);
Console.WriteLine($"Starting get api at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture)}");
var calendars = await GetData(startdate, enddate, token);
if (calendars is null)
return [];
return calendars.Where(e => string.Compare(startdate[..6], e.CalenYm) <= 0 && string.Compare(enddate[..6], e.CalenYm) >= 0).ToList();
}
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}");
return [];
}
finally
{
_semaphore.Release();
}
}
private async Task<List<Calend>?> GetData(string startdate, string enddate, CancellationToken token = default)
{
HttpClient _httpClient = new HttpClient()
{
BaseAddress = new Uri(navigationManager.BaseUri)
};
string key = $"calendar_{startdate[..4]}";
string dataFromStorage = _jSInProcessRuntime.Invoke<string>("sessionStorage.getItem", key);
if (dataFromStorage is not null)
{
return System.Text.Json.JsonSerializer.Deserialize<List<Calend>>(dataFromStorage)!;
}
var calendars = await _httpClient.GetFromJsonAsync<List<Calend>>("./calendar.json", token);
_jSInProcessRuntime.Invoke<string>("sessionStorage.setItem", key, System.Text.Json.JsonSerializer.Serialize(calendars));
return calendars;
}
}
</code>
<code>// Service public interface ICalendDataListService { Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default); } public class CalendDataListService : ICalendDataListService { private IJSInProcessRuntime _jSInProcessRuntime; private static readonly SemaphoreSlim _semaphore = new(initialCount: 1, 1); private NavigationManager navigationManager; public CalendDataListService(IJSInProcessRuntime jSInProcessRuntime, NavigationManager navigationManager) { _jSInProcessRuntime = jSInProcessRuntime; this.navigationManager = navigationManager; } public async Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default) { try { await _semaphore.WaitAsync(token); Console.WriteLine($"Starting get api at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture)}"); var calendars = await GetData(startdate, enddate, token); if (calendars is null) return []; return calendars.Where(e => string.Compare(startdate[..6], e.CalenYm) <= 0 && string.Compare(enddate[..6], e.CalenYm) >= 0).ToList(); } catch (Exception ex) { Console.WriteLine($"{ex.Message}"); return []; } finally { _semaphore.Release(); } } private async Task<List<Calend>?> GetData(string startdate, string enddate, CancellationToken token = default) { HttpClient _httpClient = new HttpClient() { BaseAddress = new Uri(navigationManager.BaseUri) }; string key = $"calendar_{startdate[..4]}"; string dataFromStorage = _jSInProcessRuntime.Invoke<string>("sessionStorage.getItem", key); if (dataFromStorage is not null) { return System.Text.Json.JsonSerializer.Deserialize<List<Calend>>(dataFromStorage)!; } var calendars = await _httpClient.GetFromJsonAsync<List<Calend>>("./calendar.json", token); _jSInProcessRuntime.Invoke<string>("sessionStorage.setItem", key, System.Text.Json.JsonSerializer.Serialize(calendars)); return calendars; } } </code>
// Service
public interface ICalendDataListService
{
    Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default);
}

public class CalendDataListService : ICalendDataListService
{
    private IJSInProcessRuntime _jSInProcessRuntime;
    private static readonly SemaphoreSlim _semaphore = new(initialCount: 1, 1);
    private NavigationManager navigationManager;
    public CalendDataListService(IJSInProcessRuntime jSInProcessRuntime, NavigationManager navigationManager)
    {
        _jSInProcessRuntime = jSInProcessRuntime;
        this.navigationManager = navigationManager;
    }

    public async Task<List<Calend>> GetDataByDays(string startdate, string enddate, CancellationToken token = default)
    {
        try
        {
            await _semaphore.WaitAsync(token);
            Console.WriteLine($"Starting get api at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture)}");
            var calendars = await GetData(startdate, enddate, token);
            if (calendars is null)
                return [];

            return calendars.Where(e => string.Compare(startdate[..6], e.CalenYm) <= 0 && string.Compare(enddate[..6], e.CalenYm) >= 0).ToList();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.Message}");
            return [];
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private async Task<List<Calend>?> GetData(string startdate, string enddate, CancellationToken token = default)
    {
        HttpClient _httpClient = new HttpClient()
        {
            BaseAddress = new Uri(navigationManager.BaseUri)
        };
        string key = $"calendar_{startdate[..4]}";
        string dataFromStorage = _jSInProcessRuntime.Invoke<string>("sessionStorage.getItem", key);
        if (dataFromStorage is not null)
        {
            return System.Text.Json.JsonSerializer.Deserialize<List<Calend>>(dataFromStorage)!;
        }

        var calendars = await _httpClient.GetFromJsonAsync<List<Calend>>("./calendar.json", token);
        _jSInProcessRuntime.Invoke<string>("sessionStorage.setItem", key, System.Text.Json.JsonSerializer.Serialize(calendars));
        return calendars;

    }
}

1

I’m assuming you’re using the semaphore to prevent two processes starting the GetData process.

As long as all the calls originate in components, you don’t need to worry. Just use and await Tasks. Why? The Blazor UI operates in a Synchronisation Context which guarantees a single virtual thread of execution. There’s only one line.

Here’s a demo using a Country/Continent context [I’ve lifted the majority of the code from a previous answer].

First the Service:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>public record Country(string country, string continent);
</code>
<code>public record Country(string country, string continent); </code>
public record Country(string country, string continent);
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>public class CountryService
{
private readonly HttpClient httpClient;
private IEnumerable<Country>? _countries;
private bool _loading = true;
private Task? _loadingTask;
public CountryService(HttpClient httpClient)
{
this.httpClient = httpClient;
}
// This will always get called from componments, and thus from the Synchronisation Context
// That guarantees a single thread of execution, so GetDataAsync
// can't get called before it yields on the firat await
// and _loadingTask is not null
public async Task<IEnumerable<Country>> GetDataAsync(string continent)
{
if (_loadingTask is null)
_loadingTask = LoadDataAsync();
await _loadingTask;
return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>();
}
private async Task LoadDataAsync()
{
// Load from Session Storage if exists
// make it sloooooow
await Task.Delay(5000);
_countries = await httpClient.GetFromJsonAsync<IEnumerable<Country>>("sample-data/countries.json") ?? Enumerable.Empty<Country>();
// Save to Session Storage if new
}
}
</code>
<code>public class CountryService { private readonly HttpClient httpClient; private IEnumerable<Country>? _countries; private bool _loading = true; private Task? _loadingTask; public CountryService(HttpClient httpClient) { this.httpClient = httpClient; } // This will always get called from componments, and thus from the Synchronisation Context // That guarantees a single thread of execution, so GetDataAsync // can't get called before it yields on the firat await // and _loadingTask is not null public async Task<IEnumerable<Country>> GetDataAsync(string continent) { if (_loadingTask is null) _loadingTask = LoadDataAsync(); await _loadingTask; return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>(); } private async Task LoadDataAsync() { // Load from Session Storage if exists // make it sloooooow await Task.Delay(5000); _countries = await httpClient.GetFromJsonAsync<IEnumerable<Country>>("sample-data/countries.json") ?? Enumerable.Empty<Country>(); // Save to Session Storage if new } } </code>
public class CountryService
{
    private readonly HttpClient httpClient;

    private IEnumerable<Country>? _countries;
    private bool _loading = true;

    private Task? _loadingTask;

    public CountryService(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    // This will always get called from componments, and thus from the Synchronisation Context
    // That guarantees a single thread of execution, so GetDataAsync
    // can't get called before it yields on the firat await
    // and _loadingTask is not null
    public async Task<IEnumerable<Country>> GetDataAsync(string continent)
    {
        if (_loadingTask is null)
            _loadingTask = LoadDataAsync();

        await _loadingTask;

        return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>();
    }

    private async Task LoadDataAsync()
    {
        // Load from Session Storage if exists
        // make it sloooooow
        await Task.Delay(5000);
        _countries = await httpClient.GetFromJsonAsync<IEnumerable<Country>>("sample-data/countries.json") ?? Enumerable.Empty<Country>();
        // Save to Session Storage if new
    }
}

Service Setup [I’m using Server]:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>builder.Services.AddScoped<CountryService>();
var services = builder.Services;
{
// Server Side Blazor doesn't register HttpClient by default
// Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
// Setup HttpClient for server side in a client side compatible fashion
services.AddScoped<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}
}
</code>
<code>builder.Services.AddScoped<CountryService>(); var services = builder.Services; { // Server Side Blazor doesn't register HttpClient by default // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { // Setup HttpClient for server side in a client side compatible fashion services.AddScoped<HttpClient>(s => { // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. var uriHelper = s.GetRequiredService<NavigationManager>(); return new HttpClient { BaseAddress = new Uri(uriHelper.BaseUri) }; }); } } </code>
builder.Services.AddScoped<CountryService>();

var services = builder.Services;
{
    // Server Side Blazor doesn't register HttpClient by default
    // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
    if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
    {
        // Setup HttpClient for server side in a client side compatible fashion
        services.AddScoped<HttpClient>(s =>
        {
            // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
            var uriHelper = s.GetRequiredService<NavigationManager>();
            return new HttpClient
            {
                BaseAddress = new Uri(uriHelper.BaseUri)
            };
        });
    }
}

And my demo page to show multiple loads against the service:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>@page "/"
@inject CountryService CountryService
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
<div class="row mb-3">
<div class="col-4">
<select class="form-select" @bind="@_filterContinent1" @bind:after="UpdateList1">
<option selected disabled>--Filter--</option>
@foreach (var continent in Continents)
{
<option value="@continent">@continent</option>
}
</select>
</div>
<div class="col-4">
<select class="form-select" @bind="@_filterContinent2" @bind:after="UpdateList2">
<option selected disabled>--Filter--</option>
@foreach (var continent in Continents)
{
<option value="@continent">@continent</option>
}
</select>
</div>
<div class="col-4">
<select class="form-select" @bind="@_filterContinent3" @bind:after="UpdateList3">
<option selected disabled>--Filter--</option>
@foreach (var continent in Continents)
{
<option value="@continent">@continent</option>
}
</select>
</div>
</div>
<div class="row mt-3">
<div class=" col-4">
<h2>Filtered List of Countries in @_filterContinent1</h2>
@foreach (var item in _countries1)
{
<div>@item.country - @item.continent</div>
}
</div>
<div class=" col-4">
<h2>Filtered List of Countries in @_filterContinent2</h2>
@foreach (var item in _countries2)
{
<div>@item.country - @item.continent</div>
}
</div>
<div class=" col-4">
<h2>Filtered List of Countries in @_filterContinent3</h2>
@foreach (var item in _countries3)
{
<div>@item.country - @item.continent</div>
}
</div>
</div>
@code {
private IEnumerable<string> Continents = new List<string> { "Africa", "Asia", "Europe", "Oceania", "North America", "South America" };
private IEnumerable<Country> _countries1 = Enumerable.Empty<Country>();
private IEnumerable<Country> _countries2 = Enumerable.Empty<Country>();
private IEnumerable<Country> _countries3 = Enumerable.Empty<Country>();
private string? _filterContinent1;
private string? _filterContinent2;
private string? _filterContinent3;
private string? FilterAuto;
private async Task UpdateList1()
{
_countries1 = await this.CountryService.GetDataAsync(_filterContinent1 ?? "None");
}
private async Task UpdateList2()
{
_countries2 = await this.CountryService.GetDataAsync(_filterContinent2 ?? "None");
}
private async Task UpdateList3()
{
_countries3 = await this.CountryService.GetDataAsync(_filterContinent3 ?? "None");
}
}
</code>
<code>@page "/" @inject CountryService CountryService <PageTitle>Home</PageTitle> <h1>Hello, world!</h1> <div class="row mb-3"> <div class="col-4"> <select class="form-select" @bind="@_filterContinent1" @bind:after="UpdateList1"> <option selected disabled>--Filter--</option> @foreach (var continent in Continents) { <option value="@continent">@continent</option> } </select> </div> <div class="col-4"> <select class="form-select" @bind="@_filterContinent2" @bind:after="UpdateList2"> <option selected disabled>--Filter--</option> @foreach (var continent in Continents) { <option value="@continent">@continent</option> } </select> </div> <div class="col-4"> <select class="form-select" @bind="@_filterContinent3" @bind:after="UpdateList3"> <option selected disabled>--Filter--</option> @foreach (var continent in Continents) { <option value="@continent">@continent</option> } </select> </div> </div> <div class="row mt-3"> <div class=" col-4"> <h2>Filtered List of Countries in @_filterContinent1</h2> @foreach (var item in _countries1) { <div>@item.country - @item.continent</div> } </div> <div class=" col-4"> <h2>Filtered List of Countries in @_filterContinent2</h2> @foreach (var item in _countries2) { <div>@item.country - @item.continent</div> } </div> <div class=" col-4"> <h2>Filtered List of Countries in @_filterContinent3</h2> @foreach (var item in _countries3) { <div>@item.country - @item.continent</div> } </div> </div> @code { private IEnumerable<string> Continents = new List<string> { "Africa", "Asia", "Europe", "Oceania", "North America", "South America" }; private IEnumerable<Country> _countries1 = Enumerable.Empty<Country>(); private IEnumerable<Country> _countries2 = Enumerable.Empty<Country>(); private IEnumerable<Country> _countries3 = Enumerable.Empty<Country>(); private string? _filterContinent1; private string? _filterContinent2; private string? _filterContinent3; private string? FilterAuto; private async Task UpdateList1() { _countries1 = await this.CountryService.GetDataAsync(_filterContinent1 ?? "None"); } private async Task UpdateList2() { _countries2 = await this.CountryService.GetDataAsync(_filterContinent2 ?? "None"); } private async Task UpdateList3() { _countries3 = await this.CountryService.GetDataAsync(_filterContinent3 ?? "None"); } } </code>
@page "/"
@inject CountryService CountryService
<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>


<div class="row mb-3">
    <div class="col-4">
        <select class="form-select" @bind="@_filterContinent1" @bind:after="UpdateList1">
            <option selected disabled>--Filter--</option>
            @foreach (var continent in Continents)
            {
                <option value="@continent">@continent</option>
            }
        </select>
    </div>
    <div class="col-4">
        <select class="form-select" @bind="@_filterContinent2" @bind:after="UpdateList2">
            <option selected disabled>--Filter--</option>
            @foreach (var continent in Continents)
            {
                <option value="@continent">@continent</option>
            }
        </select>
    </div>
    <div class="col-4">
        <select class="form-select" @bind="@_filterContinent3" @bind:after="UpdateList3">
            <option selected disabled>--Filter--</option>
            @foreach (var continent in Continents)
            {
                <option value="@continent">@continent</option>
            }
        </select>
    </div>
</div>


<div class="row mt-3">
    <div class=" col-4">
        <h2>Filtered List of Countries in @_filterContinent1</h2>
        @foreach (var item in _countries1)
        {
            <div>@item.country - @item.continent</div>
        }
    </div>
    <div class=" col-4">
        <h2>Filtered List of Countries in @_filterContinent2</h2>
        @foreach (var item in _countries2)
        {
            <div>@item.country - @item.continent</div>
        }
    </div>
    <div class=" col-4">
        <h2>Filtered List of Countries in @_filterContinent3</h2>
        @foreach (var item in _countries3)
        {
            <div>@item.country - @item.continent</div>
        }
    </div>
</div>

@code {
    private IEnumerable<string> Continents = new List<string> { "Africa", "Asia", "Europe", "Oceania", "North America", "South America" };
    private IEnumerable<Country> _countries1 = Enumerable.Empty<Country>();
    private IEnumerable<Country> _countries2 = Enumerable.Empty<Country>();
    private IEnumerable<Country> _countries3 = Enumerable.Empty<Country>();

    private string? _filterContinent1;
    private string? _filterContinent2;
    private string? _filterContinent3;
    private string? FilterAuto;

    private async Task UpdateList1()
    {
        _countries1 = await this.CountryService.GetDataAsync(_filterContinent1 ?? "None");
    }

    private async Task UpdateList2()
    {
        _countries2 = await this.CountryService.GetDataAsync(_filterContinent2 ?? "None");
    }

    private async Task UpdateList3()
    {
        _countries3 = await this.CountryService.GetDataAsync(_filterContinent3 ?? "None");
    }
}

If you did need to worry about concurrency, just use a lock like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> private readonly object _lock = new();
public async Task<IEnumerable<Country>> GetDataAsync(string continent)
{
// only allow one process access to the start the loading task
// any subsequent waiting calls will detect an existing Task
if (_loadingTask is null)
{
lock (_lock)
{
if (_loadingTask is null)
_loadingTask = LoadDataAsync();
}
}
await _loadingTask;
return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>();
}
</code>
<code> private readonly object _lock = new(); public async Task<IEnumerable<Country>> GetDataAsync(string continent) { // only allow one process access to the start the loading task // any subsequent waiting calls will detect an existing Task if (_loadingTask is null) { lock (_lock) { if (_loadingTask is null) _loadingTask = LoadDataAsync(); } } await _loadingTask; return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>(); } </code>
    private readonly object _lock = new();

    public async Task<IEnumerable<Country>> GetDataAsync(string continent)
    {
        // only allow one process access to the start the loading task
        // any subsequent waiting calls will detect an existing Task
        if (_loadingTask is null)
        {
            lock (_lock)
            {
                if (_loadingTask is null)
                    _loadingTask = LoadDataAsync();
            }
        }

        await _loadingTask;

        return _countries?.Where(item => item.continent == continent) ?? Enumerable.Empty<Country>();
    }

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật