Building a Walking Skeleton
Create Project
- Click
Blazor WebAssembly
- Click
ASP.NET Core hosted
. It will create Client, Server and Shared Projects.


Model
Create Product
-
[Column(TypeName = "decimal(18,2)")]
is for a decimal error on database. - When you use the
decimal
type in your variable, you should set a scale of decimal. -
Because the flotting point can make a misunderstand in memory.
- BlazorEcommerce.Shared/Product.cs
public class Product
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
}
Database
Packages in Server
- Download
Microsoft.EntityFrameworkCore
,Microsoft.EntityFrameworkCore.Design
andMicrosoft.EntityframeworkCore.Sqlserver
from Nuget Packages. - Install dotnet ef with a command
dotnet tool install --global dotnet-ef
.

Data Context
- BlazorEcommerce.Server/Data/DataContext.cs
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options){ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasData(
new Product
{
Id = 1,
Title = "The Hitchhiker's Guide to the Galaxy",
Description = "The Hitchhiker's Guide to the Galaxy[note 1] (sometimes referred to as HG2G,[1] HHGTTG,[2] H2G2,[3] or tHGttG) is a comedy science fiction franchise created by Douglas Adams. Originally a 1978 radio comedy broadcast on BBC Radio 4, it was later adapted to other formats, including stage shows, novels, comic books, a 1981 TV series, a 1984 text-based computer game, and 2005 feature film.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg",
Price = 9.99m
},
new Product
{
Id = 2,
Title = "Ready Player One",
Description = "Ready Player One is a 2011 science fiction novel, and the debut novel of American author Ernest Cline. The story, set in a dystopia in 2045, follows protagonist Wade Watts on his search for an Easter egg in a worldwide virtual reality game, the discovery of which would lead him to inherit the game creator's fortune. Cline sold the rights to publish the novel in June 2010, in a bidding war to the Crown Publishing Group (a division of Random House).[1] The book was published on August 16, 2011.[2] An audiobook was released the same day; it was narrated by Wil Wheaton, who was mentioned briefly in one of the chapters.[3][4]Ch. 20 In 2012, the book received an Alex Award from the Young Adult Library Services Association division of the American Library Association[5] and won the 2011 Prometheus Award.[6]",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg",
Price = 7.99m
},
new Product
{
Id = 3,
Title = "Nineteen Eighty-Four",
Description = "Nineteen Eighty-Four (also stylised as 1984) is a dystopian social science fiction novel and cautionary tale written by English writer George Orwell. It was published on 8 June 1949 by Secker & Warburg as Orwell's ninth and final book completed in his lifetime. Thematically, it centres on the consequences of totalitarianism, mass surveillance and repressive regimentation of people and behaviours within society.[2][3] Orwell, a democratic socialist, modelled the totalitarian government in the novel after Stalinist Russia and Nazi Germany.[2][3][4] More broadly, the novel examines the role of truth and facts within politics and the ways in which they are manipulated.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg",
Price = 6.99m
});
}
public DbSet<Product> Products { get; set; }
}
Set Database
- BlazorEcommerce.Server/appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BlazorEcommerce;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
},
...
-
Add information about database to server program.
-
BlazorEcommerce.Server/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
...
Update Database
- In console, tpye
dotnet ef migrations add CreateInitial
anddotnet ef database update
.

Controller
- You can your own web Api on Controller.
- Send and retuen data is json type.
- When you create new controller, click
API Controller - Empty
.

- BlazorEcommerce.Server/Controllers/ProductController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly DataContext _context;
public ProductController(DataContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<List<Product>>> GetProduct()
{
var products = await _context.Products.ToListAsync();
return Ok(products);
}
}
Swagger
Package in Server
- Download
Swashbuckle.AspNetCore
from Nuget Packages.

Set
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddRazorPages();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
...
var app = builder.Build();
app.UseSwaggerUI();
app.UseSwagger();
...
Run
- Change Url to
.../swagger/index.html
.

Show Products
Create ProductList Component
- Razor file is for view.
- BlazorEcommerce.Client/Shared/ProductList.razor
<ul class="list-unstyled">
@foreach(var product in Products)
{
<li class="media my-3">
<div class="media-img-wrapper mr-2">
<a href="#">
<img class="media-img" src="@product.ImageUrl" alt="@product.Title"/>
</a>
</div>
<div class="media-body">
<a href="#">
<h4 class="mb-0">@product.Title</h4>
</a>
<p>@product.Description</p>
<h5 class="price">
$@product.Price
</h5>
</div>
</li>
}
</ul>
- cs file is for code.
- BlazorEcommerce.Client/Shared/ProductList.razor.cs
public partial class ProductList
{
[Inject]
public HttpClient Http { get; set; }
private static List<Product> Products = new List<Product>();
protected override async Task OnInitializedAsync()
{
var result = await Http.GetFromJsonAsync<List<Product>>("api/product");
if (result != null)
Products = result;
}
}
- css file is for design.
- BlazorEcommerce.Client/Shared/ProductList.razor.css
.media-img {
max-height: 200px;
max-width: 200px;
border-radius: 6px;
margin-bottom: 10px;
transition: transform .2s;
}
.media-img:hover {
transform: scale(1.1);
}
.media {
display: flex;
align-items: flex-start;
}
.media-body{
flex: 1;
}
.media-img-wrapper {
width: 200px;
text-align: center;
}
.price {
color: green;
}
@media (max-width:1023.98px) {
.media {
flex-direction: column;
}
.media-img-wrapper{
width: 100%;
height: auto;
}
}
Add ProductList Component on Index
- BlazorEcommerce.Client/Shared/ProductList.razor
@page "/"
<PageTitle>My Shop</PageTitle>
<ProductList/>
Run

Naming Options
- Click
Tools
-Options
-Text Editor
-C#
-Code Style
-Naming
. - To create a new naming Style, click
Manage naming styles
.


Products
Models for Product Service
Models
- BlazorEcommerce.Shared/Product.cs
public class Product
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
[Column(TypeName = "decimal(18,2)")]
public Category? Category { get; set; }
public int CategoryId { get; set; }
public bool Featured { get; set; } = false;
public List<ProductVariant> Variants { get; set; } = new List<ProductVariant>();
}
- BlazorEcommerce.Shared/Category.cs
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
- BlazorEcommerce.Shared/ProductVariant.cs
public class ProductVariant
{
[JsonIgnore]
public Product Product { get; set; }
public int ProductId { get; set; }
public ProductType ProductType { get; set; }
public int ProductTypeId { get; set; }
[Column(TypeName="decimal(18,2)")]
public decimal Price { get; set; }
[Column(TypeName="decimal(18,2)")]
public decimal OriginalPrice { get; set; }
}
-
JsonIgnore
means this property is excluded from Json Serialization. -
BlazorEcommerce.Shared/ProductVariant.cs
public class ProductType
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
- BlazorEcommerce.Shared/ProductSearchResult.cs
public class ProductSearchResult
{
public List<Product> Products { get; set; } = new List<Product>();
public int Pages { get; set; }
public int CurrentPage { get; set; }
}
Seeds
- BlazorEcommerce.Server/Data/DataContext.cs
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductVariant>()
.HasKey(p => new { p.ProductId, p.ProductTypeId });
modelBuilder.Entity<ProductType>().HasData(
new ProductType { Id = 1, Name = "Default" },
new ProductType { Id = 2, Name = "Paperback" },
new ProductType { Id = 3, Name = "E-Book" },
new ProductType { Id = 4, Name = "Audiobook" },
new ProductType { Id = 5, Name = "Stream" },
new ProductType { Id = 6, Name = "Blu-ray" },
new ProductType { Id = 7, Name = "VHS" },
new ProductType { Id = 8, Name = "PC" },
new ProductType { Id = 9, Name = "PlayStation" },
new ProductType { Id = 10, Name = "Xbox" });
modelBuilder.Entity<Category>().HasData(
new Category
{
Id = 1,
Name = "Books",
Url = "books"
},
new Category
{
Id = 2,
Name = "Movies",
Url = "movies"
},
new Category
{
Id = 3,
Name = "Video Games",
Url = "video-games"
});
modelBuilder.Entity<Product>().HasData(
new Product
{
Id = 1,
CategoryId = 1,
Title = "The Hitchhiker's Guide to the Galaxy",
Description = "The Hitchhiker's Guide to the Galaxy[note 1] (sometimes referred to as HG2G,[1] HHGTTG,[2] H2G2,[3] or tHGttG) is a comedy science fiction franchise created by Douglas Adams. Originally a 1978 radio comedy broadcast on BBC Radio 4, it was later adapted to other formats, including stage shows, novels, comic books, a 1981 TV series, a 1984 text-based computer game, and 2005 feature film.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg",
Featured = true
},
new Product
{
Id = 2,
CategoryId = 1,
Title = "Ready Player One",
Description = "Ready Player One is a 2011 science fiction novel, and the debut novel of American author Ernest Cline. The story, set in a dystopia in 2045, follows protagonist Wade Watts on his search for an Easter egg in a worldwide virtual reality game, the discovery of which would lead him to inherit the game creator's fortune. Cline sold the rights to publish the novel in June 2010, in a bidding war to the Crown Publishing Group (a division of Random House).[1] The book was published on August 16, 2011.[2] An audiobook was released the same day; it was narrated by Wil Wheaton, who was mentioned briefly in one of the chapters.[3][4]Ch. 20 In 2012, the book received an Alex Award from the Young Adult Library Services Association division of the American Library Association[5] and won the 2011 Prometheus Award.[6]",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg"
},
new Product
{
Id = 3,
CategoryId = 1,
Title = "Nineteen Eighty-Four",
Description = "Nineteen Eighty-Four (also stylised as 1984) is a dystopian social science fiction novel and cautionary tale written by English writer George Orwell. It was published on 8 June 1949 by Secker & Warburg as Orwell's ninth and final book completed in his lifetime. Thematically, it centres on the consequences of totalitarianism, mass surveillance and repressive regimentation of people and behaviours within society.[2][3] Orwell, a democratic socialist, modelled the totalitarian government in the novel after Stalinist Russia and Nazi Germany.[2][3][4] More broadly, the novel examines the role of truth and facts within politics and the ways in which they are manipulated.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg"
},
new Product
{
Id = 4,
CategoryId = 2,
Title = "The Matrix",
Description = "The Matrix is a 1999 science fiction action film written and directed by the Wachowskis, and produced by Joel Silver. Starring Keanu Reeves, Laurence Fishburne, Carrie-Anne Moss, Hugo Weaving, and Joe Pantoliano, and as the first installment in the Matrix franchise, it depicts a dystopian future in which humanity is unknowingly trapped inside a simulated reality, the Matrix, which intelligent machines have created to distract humans while using their bodies as an energy source. When computer programmer Thomas Anderson, under the hacker alias \"Neo\", uncovers the truth, he \"is drawn into a rebellion against the machines\" along with other people who have been freed from the Matrix.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/c/c1/The_Matrix_Poster.jpg"
},
new Product
{
Id = 5,
CategoryId = 2,
Title = "Back to the Future",
Description = "Back to the Future is a 1985 American science fiction film directed by Robert Zemeckis. Written by Zemeckis and Bob Gale, it stars Michael J. Fox, Christopher Lloyd, Lea Thompson, Crispin Glover, and Thomas F. Wilson. Set in 1985, the story follows Marty McFly (Fox), a teenager accidentally sent back to 1955 in a time-traveling DeLorean automobile built by his eccentric scientist friend Doctor Emmett \"Doc\" Brown (Lloyd). Trapped in the past, Marty inadvertently prevents his future parents' meeting—threatening his very existence—and is forced to reconcile the pair and somehow get back to the future.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/d/d2/Back_to_the_Future.jpg",
Featured = true
},
new Product
{
Id = 6,
CategoryId = 2,
Title = "Toy Story",
Description = "Toy Story is a 1995 American computer-animated comedy film produced by Pixar Animation Studios and released by Walt Disney Pictures. The first installment in the Toy Story franchise, it was the first entirely computer-animated feature film, as well as the first feature film from Pixar. The film was directed by John Lasseter (in his feature directorial debut), and written by Joss Whedon, Andrew Stanton, Joel Cohen, and Alec Sokolow from a story by Lasseter, Stanton, Pete Docter, and Joe Ranft. The film features music by Randy Newman, was produced by Bonnie Arnold and Ralph Guggenheim, and was executive-produced by Steve Jobs and Edwin Catmull. The film features the voices of Tom Hanks, Tim Allen, Don Rickles, Wallace Shawn, John Ratzenberger, Jim Varney, Annie Potts, R. Lee Ermey, John Morris, Laurie Metcalf, and Erik von Detten. Taking place in a world where anthropomorphic toys come to life when humans are not present, the plot focuses on the relationship between an old-fashioned pull-string cowboy doll named Woody and an astronaut action figure, Buzz Lightyear, as they evolve from rivals competing for the affections of their owner, Andy Davis, to friends who work together to be reunited with Andy after being separated from him.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/1/13/Toy_Story.jpg"
},
new Product
{
Id = 7,
CategoryId = 3,
Title = "Half-Life 2",
Description = "Half-Life 2 is a 2004 first-person shooter game developed and published by Valve. Like the original Half-Life, it combines shooting, puzzles, and storytelling, and adds features such as vehicles and physics-based gameplay.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/2/25/Half-Life_2_cover.jpg"
},
new Product
{
Id = 8,
CategoryId = 3,
Title = "Diablo II",
Description = "Diablo II is an action role-playing hack-and-slash computer video game developed by Blizzard North and published by Blizzard Entertainment in 2000 for Microsoft Windows, Classic Mac OS, and macOS.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/d/d5/Diablo_II_Coverart.png"
},
new Product
{
Id = 9,
CategoryId = 3,
Title = "Day of the Tentacle",
Description = "Day of the Tentacle, also known as Maniac Mansion II: Day of the Tentacle, is a 1993 graphic adventure game developed and published by LucasArts. It is the sequel to the 1987 game Maniac Mansion.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/7/79/Day_of_the_Tentacle_artwork.jpg",
Featured = true
},
new Product
{
Id = 10,
CategoryId = 3,
Title = "Xbox",
Description = "The Xbox is a home video game console and the first installment in the Xbox series of video game consoles manufactured by Microsoft.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/4/43/Xbox-console.jpg"
},
new Product
{
Id = 11,
CategoryId = 3,
Title = "Super Nintendo Entertainment System",
Description = "The Super Nintendo Entertainment System (SNES), also known as the Super NES or Super Nintendo, is a 16-bit home video game console developed by Nintendo that was released in 1990 in Japan and South Korea.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/e/ee/Nintendo-Super-Famicom-Set-FL.jpg"
});
modelBuilder.Entity<ProductVariant>().HasData(
new ProductVariant
{
ProductId = 1,
ProductTypeId = 2,
Price = 9.99m,
OriginalPrice = 19.99m
},
new ProductVariant
{
ProductId = 1,
ProductTypeId = 3,
Price = 7.99m
},
new ProductVariant
{
ProductId = 1,
ProductTypeId = 4,
Price = 19.99m,
OriginalPrice = 29.99m
},
new ProductVariant
{
ProductId = 2,
ProductTypeId = 2,
Price = 7.99m,
OriginalPrice = 14.99m
},
new ProductVariant
{
ProductId = 3,
ProductTypeId = 2,
Price = 6.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 5,
Price = 3.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 6,
Price = 9.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 7,
Price = 19.99m
},
new ProductVariant
{
ProductId = 5,
ProductTypeId = 5,
Price = 3.99m,
},
new ProductVariant
{
ProductId = 6,
ProductTypeId = 5,
Price = 2.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 8,
Price = 19.99m,
OriginalPrice = 29.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 9,
Price = 69.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 10,
Price = 49.99m,
OriginalPrice = 59.99m
},
new ProductVariant
{
ProductId = 8,
ProductTypeId = 8,
Price = 9.99m,
OriginalPrice = 24.99m,
},
new ProductVariant
{
ProductId = 9,
ProductTypeId = 8,
Price = 14.99m
},
new ProductVariant
{
ProductId = 10,
ProductTypeId = 1,
Price = 159.99m,
OriginalPrice = 299m
},
new ProductVariant
{
ProductId = 11,
ProductTypeId = 1,
Price = 79.99m,
OriginalPrice = 399m
});
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<ProductType> ProductTypes { get; set; }
public DbSet<ProductVariant> ProductVariants { get; set; }
}
Datebase
- Enter
dotnet ef migrations add Products
anddotnet ef update database
in CLI.
Service Responce with Generics
- BlazorEcommerce.Shared/ServiceResponse.cs
public class ServiceResponse<T>
{
public T? Data { get; set; }
public bool Success { get; set; } = true;
public string Message { get; set; } = string.Empty;
}
- On
T
every variable types can be posited.
Product Service in Server
Interface
- BlazorEcommerce.Server/Services/ProductService/IProductService.cs
public interface IProductService
{
Task<ServiceResponse<List<Product>>> GetProductsAsync();
Task<ServiceResponse<Product>> GetProductAsync(int productId);
Task<ServiceResponse<List<Product>>> GetProductsByCategory(string categoryUrl);
Task<ServiceResponse<ProductSearchResult>> SearchProducts(string searchText, int page);
Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText);
Task<ServiceResponse<List<Product>>> GetFeaturedProducts();
}
Implement
- BlazorEcommerce.Server/Services/ProductService/ProductService.cs
public class ProductService : IProductService
{
private readonly DataContext _context;
public ProductService(DataContext context)
{
_context = context;
}
public async Task<ServiceResponse<List<Product>>> GetProductsAsync()
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products.Include(p => p.Variants).ToListAsync()
};
return response;
}
public async Task<ServiceResponse<Product>> GetProductAsync(int productId)
{
var response = new ServiceResponse<Product>();
var product = await _context.Products
.Include(p => p.Variants)
.ThenInclude(v => v.ProductType)
.FirstOrDefaultAsync(p => p.Id == productId);
if(product == null)
{
response.Success = false;
response.Message = "Sorry, but this product does not exist.";
}
else
{
response.Data = product;
}
return response;
}
public async Task<ServiceResponse<List<Product>>> GetProductsByCategory(string categoryUrl)
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products
.Where(p => p.Category.Url.ToLower().Equals(categoryUrl.ToLower()))
.Include(p => p.Variants)
.ToListAsync()
};
return response;
}
public async Task<ServiceResponse<ProductSearchResult>> SearchProducts
(string searchText, int page)
{
var pageResults = 2f;
var pageCount
= Math.Ceiling((await FindProductsBySearchText(searchText)).Count / pageResults);
var products = await _context.Products
.Where(p => p.Title.ToLower().Equals(searchText.ToLower())
|| p.Description.ToLower().Contains(searchText.ToLower()))
.Include(p => p.Variants)
.Skip((page - 1) * (int)pageResults)
.Take((int)pageResults)
.ToListAsync();
var response = new ServiceResponse<ProductSearchResult>
{
Data = new ProductSearchResult
{
Products = products,
CurrentPage = page,
Pages = (int)pageCount
}
};
return response;
}
private async Task<List<Product>> FindProductsBySearchText(string searchText)
{
return await _context.Products
.Where(p => p.Title.ToLower().Equals(searchText.ToLower())
|| p.Description.ToLower().Contains(searchText.ToLower()))
.Include(p => p.Variants)
.ToListAsync();
}
public async Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText)
{
var products = await FindProductsBySearchText(searchText);
List<string> result = new List<string>();
foreach(var product in products)
{
if(product.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase))
result.Add(product.Title);
if(product.Description != null)
{
var punctuation = product.Description.Where(char.IsPunctuation)
.Distinct().ToArray();
var words = product.Description.Split()
.Select(s => s.Trim(punctuation));
foreach(var word in words)
{
if(word.Contains(searchText, StringComparison.OrdinalIgnoreCase)
&& !result.Contains(word))
result.Add(word);
}
}
}
return new ServiceResponse<List<string>> { Data = result };
}
public async Task<ServiceResponse<List<Product>>> GetFeaturedProducts()
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products
.Where(p => p.Featured)
.Include(p => p.Variants)
.ToListAsync()
};
return response;
}
}
-
Include
include overlapped data to return value. -
ThenInclude
is for multi level ofInclude
. -
Math.Ceiling
round up the value. -
Skip
skips over the first n elements in the sequence and returns a new sequence containing the remaining elements after the first n elements. -
Take
extracts the first n elements from the beginning of the target sequence and returns a new sequence containing only the elements taken.
Controller
- BlazorEcommerce.Server/Controllers/ProductController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<ActionResult<ServiceResponse<List<Product>>>> GetProduct()
{
var result = await _productService.GetProductsAsync();
return Ok(result);
}
[HttpGet("{productId}")]
public async Task<ActionResult<ServiceResponse<Product>>> GetProduct(int productId)
{
var result = await _productService.GetProductAsync(productId);
return Ok(result);
}
[HttpGet("category/{categoryUrl}")]
public async Task<ActionResult<ServiceResponse<List<Product>>>> GetProductsByCategory
(string categoryUrl)
{
var result = await _productService.GetProductsByCategory(categoryUrl);
return Ok(result);
}
[HttpGet("search/{searchText}/{page}")]
public async Task<ActionResult<ServiceResponse<ProductSearchResult>>> SearchProducts
(string searchText, int page = 1)
{
var result = await _productService.SearchProducts(searchText, page);
return Ok(result);
}
[HttpGet("searchsuggestions/{searchText}")]
public async Task<ActionResult<ServiceResponse<List<string>>>> GetProductSearchSuggestions
(string searchText)
{
var result = await _productService.GetProductSearchSuggestions(searchText);
return Ok(result);
}
[HttpGet("featured")]
public async Task<ActionResult<ServiceResponse<List<Product>>>> GetFeaturedProduct()
{
var result = await _productService.GetFeaturedProducts();
return Ok(result);
}
}
Dependency Injection
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<ICartService, CartService>();
...
Product Service in Client
Interface
- BlazorEcommerce.Client/Services/ProductService/IProductService.cs
public interface IProductService
{
event Action ProductsChanged;
List<Product> Products { get; set; }
string Message { get; set; }
int CurrentPage { get; set; }
int PageCount { get; set; }
string LastSearchText { get; set; }
Task GetProducts(string? categoryUrl = null);
Task<ServiceResponse<Product>> GetProduct(int productId);
Task SearchProducts(string searchText, int page);
Task<List<string>> GetProductSearchSuggestions(string searchText);
}
Interface
- BlazorEcommerce.Client/Services/ProductService/ProductService.cs
public class ProductService : IProductService
{
private readonly HttpClient _http;
public ProductService(HttpClient http)
{
_http = http;
}
public List<Product> Products { get; set; } = new List<Product>();
public string Message { get; set; } = "Loading Products...";
public int CurrentPage { get; set; } = 1;
public int PageCount { get; set; } = 0;
public string LastSearchText { get; set; } = string.Empty;
public event Action ProductsChanged;
public async Task<ServiceResponse<Product>> GetProduct(int productId)
{
var result = await _http.GetFromJsonAsync<ServiceResponse<Product>>
($"api/product/{productId}");
return result;
}
public async Task GetProducts(string? categoryUrl = null)
{
var result = categoryUrl == null ?
await _http.GetFromJsonAsync<ServiceResponse<List<Product>>>("api/product/featured") :
await _http.GetFromJsonAsync<ServiceResponse<List<Product>>>
($"api/product/category/{categoryUrl}");
if(result != null && result.Data != null)
Products = result.Data;
CurrentPage = 1;
PageCount = 0;
if (!Products.Any())
Message = "No Product Found!";
ProductsChanged.Invoke();
}
public async Task<List<string>> GetProductSearchSuggestions(string searchText)
{
var result = await _http.GetFromJsonAsync<ServiceResponse<List<string>>>
($"api/product/searchsuggestions/{searchText}");
return result.Data;
}
public async Task SearchProducts(string searchText, int page)
{
LastSearchText = searchText;
var result = await _http.GetFromJsonAsync<ServiceResponse<ProductSearchResult>>
($"api/product/search/{searchText}/{page}");
if (result != null && result.Data != null)
{
Products = result.Data.Products;
CurrentPage = result.Data.CurrentPage;
PageCount = result.Data.Pages;
}
if (!Products.Any())
Message = "No Products found.";
ProductsChanged?.Invoke();
}
}
Dependency Injection
- BlazoreEcommerce.Client/Program.cs
...
builder.Services.AddScoped<IProductService, ProductService>();
...
Category
Category Service in Server
interface
- BlazoreEcommerce.Server/Services/CategoryService/ICategoryService.cs
public interface ICategoryService
{
Task<ServiceResponse<List<Category>>> GetCategories();
}
Implement
- BlazoreEcommerce.Server/Services/CategoryService/CategoryService.cs
public class CategoryService : ICategoryService
{
private readonly DataContext _context;
public CategoryService(DataContext context)
{
_context = context;
}
public async Task<ServiceResponse<List<Category>>> GetCategories()
{
var categories = await _context.Categories.ToListAsync();
return new ServiceResponse<List<Category>>
{
Data = categories
};
}
}
Controller
- BlazoreEcommerce.Server/Controllers/CategoryController.cs
[Route("api/[controller]")]
[ApiController]
public class CategoryController : ControllerBase
{
private readonly ICategoryService _categoryService;
public CategoryController(ICategoryService categoryService)
{
_categoryService = categoryService;
}
[HttpGet]
public async Task<ActionResult<ServiceResponse<List<Category>>>> GetCategories()
{
var result = await _categoryService.GetCategories();
return Ok(result);
}
}
Dependency Injection
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<ICategoryService, CategoryService>();
...
Category Service in Client
Interface
- BlazorEcommerce.Client/Services/CategoryService/ICategoryService.cs
public interface ICategoryService
{
List<Category> Categories { get; set; }
Task GetCategories();
}
Implement
- BlazorEcommerce.Client/Services/CategoryService/CategoryService.cs
public class CategoryService : ICategoryService
{
private readonly HttpClient _http;
public CategoryService(HttpClient http)
{
_http = http;
}
public List<Category> Categories { get; set; } = new List<Category>();
public async Task GetCategories()
{
var response = await _http.GetFromJsonAsync<ServiceResponse<List<Category>>>("api/category");
if(response != null && response.Data != null)
Categories = response.Data;
}
}
Dependency Injection
- BlazorEcommercs.Client/Program.cs
...
builder.Services.AddScoped<ICategoryService, CategoryService>();
...
Cart
Local Storage
- To save cart data, you can use local storage in bkazor application.
Package in Client
- Download
Blazored.LocalStorage
.

Set
- BlazorEcommercs.Client/Program.cs
...
builder.Services.AddBlazoredLocalStorage();
...
Models
- BlazorEcommerce.Shared/CartItem.cs
public class CartItem
{
public int ProductId { get; set; }
public int ProductTypeId { get; set; }
public int Quantity { get; set; } = 1;
}
- BlazorEcommerce.Shared/CartProductResponse.cs
public class CartProductResponse
{
public int ProductId { get; set; }
public string Title { get; set; } = string.Empty;
public int ProductTypeId { get; set; }
public string ProductType { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
}
Cart Service in Server
Interface
- BlazorEcommerce.Server/Services/ICartService.cs
public interface ICartService
{
Task<ServiceResponse<List<CartProductResponse>>> GetCartProducts(List<CartItem> cartItems);
}
Implement
- BlazorEcommerce.Server/Services/CartService.cs
public class CartService : ICartService
{
private DataContext _context;
public CartService(DataContext context)
{
_context = context;
}
public async Task<ServiceResponse<List<CartProductResponse>>> GetCartProducts
(List<CartItem> cartItems)
{
var result = new ServiceResponse<List<CartProductResponse>>()
{
Data = new List<CartProductResponse>()
};
foreach(var item in cartItems)
{
var product = await _context.Products
.Where(p => p.Id == item.ProductId)
.FirstOrDefaultAsync();
if (product == null)
continue;
var productVariant = await _context.ProductVariants
.Where(v => v.ProductId == item.ProductId
&& v.ProductTypeId == item.ProductTypeId)
.Include(v => v.ProductType)
.FirstOrDefaultAsync();
if (productVariant == null)
continue;
var cartProduct = new CartProductResponse
{
ProductId = product.Id,
Title = product.Title,
ImageUrl = product.ImageUrl,
Price = productVariant.Price,
ProductType = productVariant.ProductType.Name,
ProductTypeId = productVariant.ProductTypeId,
Quantity = item.Quantity
};
result.Data.Add(cartProduct);
}
return result;
}
}
Dependency Injection
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<ICartService, CartService>();
...
Cart Service in Client
Interface
- BlazorEcommerce.Client/Services/ICartService.cs
public interface ICartService
{
event Action OnChange;
Task AddToCart(CartItem cartItem);
Task<List<CartItem>> GetCartItems();
Task<List<CartProductResponse>> GetCartProducts();
Task RemoveProductFromCart(int productId, int productTypeId);
Task UpdateQuantity(CartProductResponse product);
}
- BlazorEcommerce.Client/Services/CartService.cs
public class CartService : ICartService
{
ILocalStorageService _localStorage;
private readonly HttpClient _http;
public CartService(ILocalStorageService localStorage, HttpClient http)
{
_localStorage = localStorage;
_http = http;
}
public event Action OnChange;
public async Task AddToCart(CartItem cartItem)
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if(cart == null)
cart = new List<CartItem>();
var sameItem = cart.Find(x => x.ProductId == cartItem.ProductId &&
x.ProductTypeId == cartItem.ProductTypeId);
if(sameItem == null)
cart.Add(cartItem);
else
sameItem.Quantity += cartItem.Quantity;
await _localStorage.SetItemAsync("cart", cart);
OnChange.Invoke();
}
public async Task<List<CartItem>> GetCartItems()
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
cart = new List<CartItem>();
return cart;
}
public async Task<List<CartProductResponse>> GetCartProducts()
{
var cartItems = await _localStorage.GetItemAsync<List<CartItem>>("cart");
var response = await _http.PostAsJsonAsync("api/cart/products", cartItems);
var cartProducts =
await response.Content.ReadFromJsonAsync<ServiceResponse<List<CartProductResponse>>>();
return cartProducts.Data;
}
public async Task RemoveProductFromCart(int productId, int productTypeId)
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
return;
var cartItem = cart.Find(x => x.ProductId == productId
&& x.ProductTypeId == productTypeId);
if (cartItem != null)
{
cart.Remove(cartItem);
await _localStorage.SetItemAsync("cart", cart);
OnChange.Invoke();
}
}
public async Task UpdateQuantity(CartProductResponse product)
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
return;
var cartItem = cart.Find(x => x.ProductId == product.ProductId
&& x.ProductTypeId == product.ProductTypeId);
if (cartItem != null)
{
cartItem.Quantity = product.Quantity;
await _localStorage.SetItemAsync("cart", cart);
}
}
}
-
GetItemAsync
get data from local storage with key word. -
SetItemAsync
set data to local storage with key word. -
api/cart/products
is a post method in server. It will return data enveloped in Content.
Dependency Injection
- BlazorEcommerce.Client/Program.cs
...
builder.Services.AddScoped<ICartService, CartService>();
...
View
Layout
Layout
- BlazorEcommerce.Client/Shared/ShopLayout.razor
<div class="page">
<main>
<div class="top-row px-2">
<HomeButton/>
<Search/>
<CartCounter/>
</div>
<div class="nav-menu">
<ShopNavMenu />
</div>
<article class="content px-2">
@Body
</article>
</main>
</div>
- BlazorEcommerce.Client/Shared/ShopLayout.razor.cs
public partial class ShopLayout : ComponentBase
{
[Parameter]
public RenderFragment? Body { get; set; }
}
- BlazorEcommerce.Client/Shared/ShopLayout.razor.css
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.nav-menu {
background-image: linear-gradient(180deg, rgb(44, 62, 80) 0%, rgb(52, 73, 94) 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: .5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row.auth {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
Set Layout in App
- BlazorEcommerce.Client/App.razor
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(ShopLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(ShopLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Home
Home Button
- BlazorEcommerce.Client/Shared/HomeButton.razor
<button @onclick="GoToHome"
class="btn btn-outline-primary home-button">
My Shop
</button>
- BlazorEcommerce.Client/Shared/HomeButton.razor.cs
public partial class HomeButton
{
[Inject]
public NavigationManager NavigationManager { get; set; }
private void GoToHome()
{
NavigationManager.NavigateTo("");
}
}
- BlazorEcommerce.Client/Shared/HomeButton.razor.css
.home-button {
white-space: nowrap;
margin: 10px;
transform: rotate(-5deg);
}
Search
- BlazorEcommerce.Client/Shared/Search.razor
<div class="input-group">
<input @bind-value="searchText"
@bind-value:event="oninput"
type="search"
list="products"
@onkeyup="HandleSearch"
class="form-control"
placeholder="Search..."
@ref="searchInput" />
<datalist id="products">
@foreach(var suggestion in suggestions)
{
<option>@suggestion</option>
}
</datalist>
<div class="input-group-append">
<button class="btn btn-primary" @onclick="SearchProducts">
<span class="oi oi-magnifying-glass"></span>
</button>
</div>
</div>
- BlazorEcommerce.Client/Shared/Search.razor.cs
public partial class Search
{
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public IProductService ProductService { get; set; }
private string searchText = string.Empty;
private List<string> suggestions = new List<string>();
protected ElementReference searchInput;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
await searchInput.FocusAsync();
}
public void SearchProducts()
{
NavigationManager.NavigateTo($"search/{searchText}/1");
}
public async Task HandleSearch(KeyboardEventArgs args)
{
if (args.Key == null || args.Key.Equals("Enter"))
SearchProducts();
else if (searchText.Any())
suggestions = await ProductService.GetProductSearchSuggestions(searchText);
}
}
Cart Counter
- BlazorEcommerce.Client/Shared/CartCounter.razor
<a href="cart" class="btn btn-info">
<i class="oi oi-cart"></i>
<span class="badge">@GetCartItemCount()</span>
</a>
- BlazorEcommerce.Client/Shared/CartCounter.razor.cs
public partial class CartCounter : IDisposable
{
[Inject]
ICartService CartService { get; set; }
[Inject]
ISyncLocalStorageService LocalStorage { get; set; }
private int GetCartItemCount()
{
var cart = LocalStorage.GetItem<List<CartItem>>("cart");
return cart != null ? cart.Count : 0;
}
protected override void OnInitialized()
{
CartService.OnChange += StateHasChanged;
}
public void Dispose()
{
CartService.OnChange -= StateHasChanged;
}
}
-
IDisposable
implements to disubscreib the event.
Shop Nav Menu
- BlazorEcommerce.Client/Shared/ShopNavMenu.razor
<div class="top-row ps-3 navbar navbar-dark navbar-toggler-wrapper">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorEcommerce</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-nav">
<div class="nav-item px-2">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
Home
</NavLink>
</div>
@foreach(var category in CategoryService.Categories)
{
<div class="nav-item px-2">
<NavLink class="nav-link" href="@category.Url">
@category.Name
</NavLink>
</div>
}
</nav>
</div>
- BlazorEcommerce.Client/Shared/ShopNavMenu.razor.cs
public partial class ShopNavMenu
{
[Inject]
public ICategoryService CategoryService { get; set; }
protected override async Task OnInitializedAsync()
{
await CategoryService.GetCategories();
}
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
- BlazorEcommerce.Client/Shared/ShopNavMenu.razor.css
.navbar-toggler {
color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.flex-nav{
flex-direction: column;
}
@media (min-width: 641px) {
.navbar-toggler, .navbar-toggler-wrapper {
display: none;
}
.flex-nav {
flex-direction: row;
display: flex;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
}
Body
- BlazorEcommerce.Client/Pages/Index.razor
@page "/"
@page "/{categoryUrl}"
@page "/search/{searchText}/{page:int}"
@if(SearchText == null && CategoryUrl == null)
{
<FeaturedProducts/>
}
else
{
<ProductList/>
}
- BlazorEcommerce.Client/Pages/Index.razor.cs
public partial class Index
{
[Inject]
public IProductService ProductService { get; set; }
[Parameter]
public string? CategoryUrl { get; set; } = null;
[Parameter]
public string? SearchText { get; set; } = null;
[Parameter]
public int Page { get; set; } = 1;
protected override async Task OnParametersSetAsync()
{
if(SearchText != null)
await ProductService.SearchProducts(SearchText, Page);
else
await ProductService.GetProducts(CategoryUrl);
}
}
Products
Featured Products
- BlazorEcommerce.Client/Shared/FeaturedProducts.razor
<center><h2>Top Products of Today</h2></center>
@if (!ProductService.Products.Any())
{
<span>@ProductService.Message</span>
}
else
{
<div class="container">
@foreach(var product in ProductService.Products)
{
@if (product.Featured)
{
<div class="featured-product">
<div>
<a href="product/@product.Id">
<img src="@product.ImageUrl">
</a>
</div>
<h4><a href="product/@product.Id">@product.Title</a></h4>
@if(product.Variants.Any())
{
<h5 class="price">
$@product.Variants[0].Price
</h5>
}
</div>
}
}
</div>
}
- BlazorEcommerce.Client/Shared/FeaturedProducts.razor.cs
public partial class FeaturedProducts : IDisposable
{
[Inject]
public IProductService ProductService { get; set; }
protected override void OnInitialized()
{
ProductService.ProductsChanged += StateHasChanged;
}
public void Dispose()
{
ProductService.ProductsChanged -= StateHasChanged;
}
}
- BlazorEcommerce.Client/Shared/FeaturedProducts.razor.css
.container {
display: flex;
flex-direction: row;
overflow-x: auto;
justify-content: center;
}
img {
max-width: 200px;
max-height: 200px;
border-radius: 6px;
transition: transform .2s;
margin-bottom: 10px;
}
img:hover{
transform: scale(1.1) rotate(5deg);
}
.featured-product {
margin: 10px;
text-align: center;
padding: 10px;
border: 1px solid lightgray;
border-radius: 10px;
max-width: 200px;
}
@media (max-width: 1023.98px) {
.container {
justify-content: flex-start;
}
}
Product List
- BlazorEcommerce.Client/Shared/ProductList.razor
@if (!ProductService.Products.Any())
{
<span>@ProductService.Message</span>
}
else
{
<ul class="list-unstyled">
@foreach(var product in ProductService.Products)
{
<li class="media my-3">
<div class="media-img-wrapper mr-2">
<a href="/product/@product.Id">
<img class="media-img" src="@product.ImageUrl" alt="@product.Title"/>
</a>
</div>
<div class="media-body">
<a href="/product/@product.Id">
<h4 class="mb-0">@product.Title</h4>
</a>
<p>@product.Description</p>
<h5 class="price">
@GetPriceText(product)
</h5>
</div>
</li>
}
</ul>
{
for(var i = 1; i <= ProductService.PageCount; i++)
{
<a class="btn
@(i == ProductService.CurrentPage ? "btn-info" : "btn-outline-info")
page-selection"
href="/search/@ProductService.LastSearchText/@i">@i</a>
}
}
}
- BlazorEcommerce.Client/Shared/ProductList.razor.cs
public partial class ProductList : IDisposable
{
[Inject]
public IProductService ProductService { get; set; }
protected override void OnInitialized()
{
ProductService.ProductsChanged += StateHasChanged;
}
public void Dispose()
{
ProductService.ProductsChanged -= StateHasChanged;
}
private string GetPriceText(Product product)
{
var variants = product.Variants;
if (!variants.Any())
return string.Empty;
else if (variants.Count == 1)
return $"${variants[0].Price}";
decimal minPrice = variants.Min(v => v.Price);
return $"Starting at ${minPrice}";
}
}
- BlazorEcommerce.Client/Shared/ProductList.razor.css
.media-img {
max-height: 200px;
max-width: 200px;
border-radius: 6px;
margin-bottom: 10px;
transition: transform .2s;
}
.media-img:hover {
transform: scale(1.1);
}
.media-img-wrapper {
width: 200px;
text-align: center;
}
.page-selection {
margin-right: 15px;
margin-bottom: 30px;
}
@media (max-width:1023.98px) {
.media {
flex-direction: column;
}
.media-img-wrapper{
width: 100%;
height: auto;
}
}
Product Detail
- BlazorEcommerce.Client/Pages/ProductDetails.razor
@page "/product/{id:int}"
@if(product == null)
{
<span>@message</span>
}
else
{
<div class="media">
<div class="media-img-wrapper mr-2">
<img class="media-img" src="@product.ImageUrl" alt="@product.Title"/>
</div>
<div class="media-body">
<h2 class="mb-0">@product.Title</h2>
<p>@product.Description</p>
@if (product.Variants.Any())
{
<div class="mb-3">
<select class="form-select" @bind="currentTypeId">
@foreach(var variant in product.Variants)
{
<option value="@variant.ProductTypeId">@variant.ProductType.Name</option>
}
</select>
</div>
}
@if(GetSelectedVariant() != null)
{
@if(GetSelectedVariant().OriginalPrice > GetSelectedVariant().Price)
{
<h6 class="text-muted original-price">
$@GetSelectedVariant().OriginalPrice
</h6>
}
<h4 class="price">
$@GetSelectedVariant().Price
</h4>
}
<button class="btn btn-primary" @onclick="AddToCart">
<i class="oi oi-cart"></i> Add to Cart
</button>
</div>
</div>
}
- BlazorEcommerce.Client/Pages/ProductDetails.razor.cs
public partial class ProductDetails
{
[Inject]
public ICartService CartService { get; set; }
[Inject]
public IProductService ProductService { get; set; }
private Product? product = null;
private string message;
private int currentTypeId = 1;
[Parameter]
public int Id { get; set; }
protected override async Task OnParametersSetAsync()
{
message = "Loading roduct...";
var result = await ProductService.GetProduct(Id);
if (!result.Success)
message = result.Message;
else
{
product = result.Data;
if (product.Variants.Any())
currentTypeId = product.Variants[0].ProductTypeId;
}
}
private ProductVariant GetSelectedVariant()
{
var variant = product.Variants.FirstOrDefault(v => v.ProductTypeId == currentTypeId);
return variant;
}
private async Task AddToCart()
{
var productVariant = GetSelectedVariant();
var cartItem = new CartItem
{
ProductId = productVariant.ProductId,
ProductTypeId = productVariant.ProductTypeId
};
await CartService.AddToCart(cartItem);
}
}
- BlazorEcommerce.Client/Pages/ProductDetails.razor.css
.media-img {
max-height: 500px;
max-width: 500px;
border-radius: 6px;
margin: 0 10px 10px 10px;
}
.media-img-wrapper {
max-width: 500px;
max-height: 500px;
text-align: center;
}
.original-price {
text-decoration: line-through;
}
@media (max-width: 1023.98px) {
.media {
flex-direction: column;
}
.media-img {
max-width: 300px;
}
.media-img-wrapper {
width: 100%;
height: auto;
}
}
Cart
- BlazorEcommerce.Client/Pages/Cart.razor
@page "/cart"
<h3>Shopping Cart</h3>
@if (!cartProducts.Any())
{
<span>@message</span>
}
else
{
<div>
@foreach(var product in cartProducts)
{
<div class="container">
<div class="image-wrapper">
<img src="@product.ImageUrl" class="image"/>
</div>
<div class="name">
<h5><a href="/product/@product.ProductId">@product.Title</a></h5>
<span>@product.ProductType</span><br/>
<input type="number"
value="@product.Quantity"
@onchange="@((ChangeEventArgs e) => UpdateQuantity(e, product))"
class="form-control input-quantity"
min="1"/>
<button class="btn-delete"
@onclick="@(()
=> RemoveProductFromCart(product.ProductId, product.ProductTypeId))">
Delete</button>
</div>
<div class="cart-product-price">$@(product.Price * product.Quantity)</div>
</div>
}
<div class="cart-product-price">
Total (@cartProducts.Count): $@cartProducts.Sum(product =>
@product.Price * product.Quantity)
</div>
</div>
}
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class Cart
{
[Inject]
public ICartService CartService { get; set; }
List<CartProductResponse> cartProducts = new List<CartProductResponse>();
string message = "Loading cart...";
protected override async Task OnInitializedAsync()
{
await LoadCart();
}
private async Task RemoveProductFromCart(int productId, int productTypeId)
{
await CartService.RemoveProductFromCart(productId, productTypeId);
await LoadCart();
}
private async Task LoadCart()
{
if (!(await CartService.GetCartItems()).Any())
{
message = "Your cart is empty.";
cartProducts = new List<CartProductResponse>();
}
else
{
cartProducts = await CartService.GetCartProducts();
}
}
private async Task UpdateQuantity(ChangeEventArgs e, CartProductResponse product)
{
product.Quantity = int.Parse(e.Value.ToString());
if (product.Quantity < 1)
product.Quantity = 1;
await CartService.UpdateQuantity(product);
}
}
- BlazorEcommerce.Client/Pages/Cart.razor.css
.container {
display: flex;
padding: 6px;
}
.image-wrapper {
width: 150px;
text-align: center;
}
.image {
max-height: 150px;
max-width: 150px;
padding: 6px;
}
.name{
flex-grow: 1;
padding: 6px;
}
.cart-product-price {
font-weight: 600;
text-align: right;
}
.btn-delete {
background: none;
border: none;
padding: 0px;
color: red;
font-size: 12px;
}
.btn-delete:hover {
text-decoration: underline;
}
.input-quantity{
width: 70px;
}
Show





Auth
Models
- BlazorEcommerce.Shared/User.cs
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public DateTime DateCreated { get; set; } = DateTime.Now;
}
-
PasswordSalt
is added characters in your original password. - For example, your original password is yellow. But, you know, others also can use yellow for password. To avoid same passwords, we add salt like yellow#1Gn% or yellow9j?L.
-
So hashed
PasswordSalt
is powerful than normal securities. - BlazorEcommerce.Shared/UserRegister.cs
public class UserRegister
{
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The passwords do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
- BlazorEcommerce.Shared/UserLogin.cs
public class UserLogin
{
[Required]
public string Email { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
- BlazorEcommerce.Shared/UserLogin.cs
public class UserChangePassword
{
[Required, StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The password do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
Set
- BlazorEcommerce.Server/Data/DataContext.cs
...
public DbSet<User> Users { get; set; }
Datebase
- Enter
dotnet ef migrations add Users
anddotnet ef update database
in CLI.
Auth Service in Server
Package in Server
- Download
Microsoft.AspNetCore.Authentication.JwtBearer
.

Interface
- BlazorEcommerce.Server/Services/AuthService/IAuthService.cs
public interface IAuthService
{
Task<ServiceResponse<int>> Register(User user, string password);
Task<bool> UserExists(string email);
Task<ServiceResponse<string>> Login(string email, string password);
Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword);
}
Implement
- BlazorEcommerce.Server/Services/AuthService/AuthService.cs
public class AuthService : IAuthService
{
private readonly DataContext _context;
private readonly IConfiguration _configuration;
public AuthService(DataContext context, IConfiguration configuration)
{
_context = context;
_configuration = configuration;
}
public async Task<ServiceResponse<int>> Register(User user, string password)
{
if(await UserExists(user.Email))
return new ServiceResponse<int>
{
Success = false,
Message = "User already exists."
};
CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
await _context.SaveChangesAsync();
return new ServiceResponse<int> { Data = user.Id, Message = "Registration successful!" };
}
public async Task<ServiceResponse<string>> Login(string email, string password)
{
var response = new ServiceResponse<string>();
var user = await _context.Users
.FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower()));
if(user == null)
{
response.Success = false;
response.Message = "User not found.";
}
else if(!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "Wrong password.";
}
else
response.Data = CreateToken(user);
return response;
}
public async Task<bool> UserExists(string email)
{
if (await _context.Users.AnyAsync(user => user.Email.ToLower()
.Equals(email.ToLower())))
return true;
return false;
}
private void CreatePasswordHash
(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
using(var hmac = new HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac
.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
{
using (var hmac = new HMACSHA512(passwordSalt))
{
var computedHash =
hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
return computedHash.SequenceEqual(passwordHash);
}
}
private string CreateToken(User user)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email)
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8
.GetBytes(_configuration.GetSection("AppSettings:Token").Value));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword)
{
var user = await _context.Users.FindAsync(userId);
if (user == null)
return new ServiceResponse<bool>
{
Success = false,
Message = "User not found."
};
CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[] passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
await _context.SaveChangesAsync();
return new ServiceResponse<bool>
{
Data = true,
Message = "Password has been changed."
};
}
}
-
IConfiguration
reads appsettings.json. -
out
reference object address but the variable should be setted. -
ref
reference object address also, but the variable should have value before. -
out
andref
are useful when you have more than two return values. -
HMACSHA512
is a type of keyed hash algorithm that is constructed from the SHA-512 hash function and used as a Hash-based Message Authentication Code (HMAC). -
Claim
is a base of the common authorization approache and provides a way of sharing user information throughout the application in a consistent way. -
SymmetricSecurityKey
generates security key. -
SigningCredentials
specifies the signing key, signing key identifier, and security algorithms that are used by .NET to generate the digital signature for a SamlAssertion.
Controller
- BlazorEcommerce.Server/Controllers/AuthController.cs
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("register")]
public async Task<ActionResult<ServiceResponse<int>>> Register(UserRegister request)
{
var response = await _authService.Register(new User
{
Email = request.Email
},
request.Password);
if (!response.Success)
return BadRequest(response);
return Ok(response);
}
[HttpPost("login")]
public async Task<ActionResult<ServiceResponse<string>>> Login(UserLogin request)
{
var response = await _authService.Login(request.Email, request.Password);
if (!response.Success)
return BadRequest(response);
return Ok(response);
}
[HttpPost("change-password"), Authorize]
public async Task<ActionResult<ServiceResponse<bool>>> ChangePassword
([FromBody] string newPassword)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var response = await _authService.ChangePassword(int.Parse(userId), newPassword);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
}
Dependency Injection
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8
.GetBytes(builder.Configuration.GetSection("AppSettings:Token").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
...
app.UseAuthentication();
app.UseAuthorization();
...
Set
- BlazorEcommerce.Server/appsettings.json
...
"AppSettings": {
"Token": "my top secret key"
},
...
Auth Service in Client
Interface
- BlazorEcommerce.Client/Services/AuthService/IAuthService.cs
public interface IAuthService
{
Task<ServiceResponse<int>> Register(UserRegister request);
Task<ServiceResponse<string>> Login(UserLogin request);
Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request);
}
Implement
- BlazorEcommerce.Client/Services/AuthService/AuthService.cs
public class AuthService : IAuthService
{
private readonly HttpClient _http;
public AuthService(HttpClient http)
{
_http = http;
}
public async Task<ServiceResponse<int>> Register(UserRegister request)
{
var result = await _http.PostAsJsonAsync("api/auth/register", request);
return await result.Content.ReadFromJsonAsync<ServiceResponse<int>>();
}
public async Task<ServiceResponse<string>> Login(UserLogin request)
{
var result = await _http.PostAsJsonAsync("api/auth/login", request);
return await result.Content.ReadFromJsonAsync<ServiceResponse<string>>();
}
public async Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request)
{
var result = await _http.PostAsJsonAsync("api/auth/change-password", request.Password);
return await result.Content.ReadFromJsonAsync<ServiceResponse<bool>>();
}
}
Dependency Injection
- BlazorEcommerce.Client/Program.cs
...
builder.Services.AddScoped<IAuthService, AuthService>();
...
Authorization
- For authorization, I will use Claim, JSON Web Token and Local Storage.
Package for Authorization
- Download
Microsoft.AspNetCore.Components.Authorization
andMicrosoft.AspNetCore.WebUtilities
in Client.

Auth State Provider
- BlazorEcommerce.Client/CustomAuthStateProvider.cs
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorageService;
private readonly HttpClient _http;
public CustomAuthStateProvider(ILocalStorageService localStorageService, HttpClient http)
{
_localStorageService = localStorageService;
_http = http;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
string authToken = await _localStorageService.GetItemAsStringAsync("authToken");
var identity = new ClaimsIdentity();
_http.DefaultRequestHeaders.Authorization = null;
if (!string.IsNullOrEmpty(authToken))
{
try
{
identity = new ClaimsIdentity(ParseClaimsFromJwt(authToken), "jwt");
_http.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", authToken.Replace("\"", ""));
}
catch
{
await _localStorageService.RemoveItemAsync("authToken");
identity = new ClaimsIdentity();
}
}
var user = new ClaimsPrincipal(identity);
var state = new AuthenticationState(user);
NotifyAuthenticationStateChanged(Task.FromResult(state));
return state;
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch(base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer
.Deserialize<Dictionary<string, object>>(jsonBytes);
var claims = keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
return claims;
}
}
-
AuthenticationStateProvider
class provides information about the authentication state of the current user. - We have to split jwt because of
HMACSHA512(base64UrlEncode(header) + "." + base64UrlEncode(payload))
. - Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens. The name “Bearer authentication” can be understood as “give access to the bearer of this token.” The bearer token is a cryptic string, usually generated by the server in response to a login request. The client must send this token in the Authorization header when making requests to protected resources
-
ClaimsPrincipal
means user andClaimsIdentity
means user information. So, we get user information from JWT first, and then create user with this user information. - And we will use
AuthorizeView
in our project, soAuthenticationState
andNotifyAuthenticationStateChanged
are for change authentication state.
Set
- BlazorEcommerce.Client/Program.cs
...
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
...
- BlazorEcommerce.Client/_imports.cs
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
View
Register
- BlazorEcommerce.Client/Pages/Register.razor
@page "/register"
<EditForm Model="user" OnValidSubmit="HandleRegistration">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
<ValidationMessage For="@(() => user.Email)"/>
</div>
<div class="mb-3">
<label for="password">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-control" type="password" />
<ValidationMessage For="@(() => user.Password)"/>
</div>
<div class="mb-3">
<label for="confirmPassword">Confirm Password</label>
<InputText id="confirmPassword" @bind-Value="user.ConfirmPassword" class="form-control"
type="password" />
<ValidationMessage For="@(() => user.ConfirmPassword)"/>
</div>
<button type="submit" class="btn btn-primary">Register</button>
<div class="@messageCssClass">
<span>@message</span>
</div>
</EditForm>
- BlazorEcommerce.Client/Pages/Register.razor.cs
public partial class Register
{
[Inject]
public IAuthService AuthService { get; set; }
UserRegister user = new UserRegister();
string message = string.Empty;
string messageCssClass = string.Empty;
async Task HandleRegistration()
{
var result = await AuthService.Register(user);
message = result.Message;
if(result.Success)
messageCssClass = "text-success";
else
messageCssClass = "text-danger";
}
}
Login
- BlazorEcommerce.Client/Pages/Login.razor
@page "/login"
<EditForm Model="user" OnValidSubmit="HandleLogin">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
<ValidationMessage For="@(() => user.Email)"/>
</div>
<div class="mb-3">
<label for="password">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-control" type="password" />
<ValidationMessage For="@(() => user.Password)"/>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</EditForm>
<div class="text-danger">
<span>@errorMessage</span>
</div>
- BlazorEcommerce.Client/Pages/Login.razor.cs
public partial class Login
{
[Inject]
public IAuthService AuthService { get; set; }
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public AuthenticationStateProvider AuthenticationStateProvider { get; set; }
private UserLogin user = new UserLogin();
private string errorMessage = string.Empty;
private string returnUrl = string.Empty;
protected override void OnInitialized()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
if(QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var url))
returnUrl = url;
}
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if(result.Success)
{
errorMessage = string.Empty;
await LocalStorage.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo(returnUrl);
}
else
errorMessage = result.Message;
}
}
- When you success the login, then you will return to
returnUrl
. QueryHelter is for this process.
Profile
- BlazorEcommerce.Client/Pages/Profile.razor
@page "/profile"
<AuthorizeView>
<h3>Hi! You're logged in with <i>@context.User.Identity.Name</i>.</h3>
</AuthorizeView>
<h5>Change Password</h5>
<EditForm Model="request" OnValidSubmit="ChangePassword">
<DataAnnotationsValidator/>
<div class="mb-3">
<label for="password">New Password</label>
<InputText id="password" @bind-Value="request.Password" class="form-control"
type="password"/>
<ValidationMessage For="@(() => request.Password)"/>
</div>
<div class="mb-3">
<label for="confirmPassword">Confirm New Password</label>
<InputText id="confirmPassword" @bind-Value="request.ConfirmPassword"
class="form-control" type="password"/>
<ValidationMessage For="@(() => request.ConfirmPassword)"/>
</div>
<button type="submit" class="btn btn-primary">Apply</button>
</EditForm>
@message
- BlazorEcommerce.Client/Pages/Profile.razor.cs
public partial class Login
{
[Authorize]
public partial class Profile
{
[Inject]
public IAuthService AuthService { get; set; }
UserChangePassword request = new UserChangePassword();
string message = string.Empty;
private async Task ChangePassword()
{
var result = await AuthService.ChangePassword(request);
message = result.Message;
}
}
}
-
[Authorize]
means this page is for authorized user.
User Button
- BlazorEcommerce.Client/Shared/UserButton.razor
<div class="dropdown">
<button @onclick="ToggleUserMenu"
@onfocusout="HideUserMenu"
class="btn btn-secondary dropdown-toggle user-button">
<i class="oi oi-person"></i>
</button>
<div class="dropdown-menu dropdown-menu-right @UserMenuCssClass">
<AuthorizeView>
<Authorized>
<a href="profile" class="dropdown-item">Profile</a>
<hr/>
<button class="dropdown-item" @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="login?returnUrl=@NavigationManager.ToBaseRelativePath(NavigationManager.Uri)"
class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
</AuthorizeView>
</div>
</div>
-
login?returnUrl=@NavigationManager.ToBaseRelativePath(NavigationManager.Uri)
means save current url to returnUrl variable in hyperlink url but go to login. -
BlazorEcommerce.Client/Shared/UserButton.razor.cs
ppublic partial class UserButton
{
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public AuthenticationStateProvider AuthenticationStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private bool showUserMenu = false;
private string UserMenuCssClass => showUserMenu ? "show-menu" : null;
private void ToggleUserMenu()
{
showUserMenu = !showUserMenu;
}
private async Task HideUserMenu()
{
await Task.Delay(200);
showUserMenu = false;
}
private async Task Logout()
{
await LocalStorage.RemoveItemAsync("authToken");
await AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo("");
}
}
- BlazorEcommerce.Client/Shared/UserButton.razor.css
.show-menu {
display: block;
}
.user-button {
margin-left: .5em;
}
.top-row a {
margin-left: 0;
}
.dropdown-item:hover {
background-color: white;
}
- BlazorEcommerce.Client/App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(ShopLayout)">
<NotAuthorized>
<h3>Whoops! You're not allowed to see this page.</h3>
<h5>Please <a href="login">login</a> or <a href="register">register</a>
for a new account.</h5>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(ShopLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Run
User Button

Register



Login



- You can check your JSON Web Token in jwt.io.
Profile



Refactoring Auth Service
-
public int GetUserId() => int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
returns UserId which is logged in now. - I will use it only in AuthService and use AuthService in other services.
Refactoring Auth Service in Server
Interface
- BlazorEcommerce.Server/Services/AuthService/IAuthService.cs
public interface IAuthService
{
...
int GetUserId();
}
Implement
- BlazorEcommerce.Server/Services/AuthService/AuthService.cs
public class AuthService : IAuthService
{
private readonly DataContext _context;
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthService(
DataContext context,
IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_context = context;
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
public int GetUserId()
=> int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
...
}
Set
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddHttpContextAccessor();
...
Refactoring Auth Service in Client
Interface
- BlazorEcommerce.Client/Services/AuthService/IAuthService.cs
public interface IAuthService
{
...
Task<bool> IsUserAuthenticated();
}
Implement
- BlazorEcommerce.Client/Services/AuthService/AuthService.cs
public class AuthService : IAuthService
{
private readonly HttpClient _http;
private readonly AuthenticationStateProvider _authStateProvider;
public AuthService(HttpClient http, AuthenticationStateProvider authStateProvider)
{
_http = http;
_authStateProvider = authStateProvider;
}
...
public async Task<bool> IsUserAuthenticated()
=> (await _authStateProvider.GetAuthenticationStateAsync()).User.Identity.IsAuthenticated;
}
Cart with Authentication and Database
Models
-
To add authentication, add UserId in CartItem.
-
BlazorEcommerce.Shared/CartItem.cs
public class CartItem
{
public int UserId { get; set; }
public int ProductId { get; set; }
public int ProductTypeId { get; set; }
public int Quantity { get; set; } = 1;
}
Set
- BlazorEcommerce.Server/Data/DataContext.cs
public class DataContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CartItem>()
.HasKey(ci => new { ci.UserId, ci.ProductId, ci.ProductTypeId });
...
}
...
public DbSet<CartItem> CartItems { get; set; }
}
Database
- Type
dotnet ef migrations add CartItems
anddotnet ef database update
Cart Service in Server
Interface
- BlazorEcommerce.Server/Services/CartService/ICartService.cs
public interface ICartService
{
...
Task<ServiceResponse<List<CartProductResponse>>> StoreCartItems(List<CartItem> cartItems);
Task<ServiceResponse<int>> GetCartItemsCount();
Task<ServiceResponse<List<CartProductResponse>>> GetDbCartProducts();
Task<ServiceResponse<bool>> AddToCart(CartItem cartItem);
Task<ServiceResponse<bool>> UpdateQuantity(CartItem cartItem);
Task<ServiceResponse<bool>> RemoveItemFromCart(int productId, int productTypeId);
}
Implement
- BlazorEcommerce.Server/Services/CartService/CartService.cs
public class CartService : ICartService
{
private readonly DataContext _context;
private readonly IAuthService _authService;
public CartService(DataContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
...
public async Task<ServiceResponse<List<CartProductResponse>>> StoreCartItems
(List<CartItem> cartItems)
{
cartItems.ForEach(cartItem => cartItem.UserId = _authService.GetUserId());
_context.CartItems.AddRange(cartItems);
await _context.SaveChangesAsync();
return await GetDbCartProducts();
}
public async Task<ServiceResponse<int>> GetCartItemsCount()
{
var count = await _context.CartItems
.Where(ci => ci.UserId == _authService.GetUserId()).CountAsync();
return new ServiceResponse<int> { Data = count };
}
public async Task<ServiceResponse<List<CartProductResponse>>> GetDbCartProducts() =>
await GetCartProducts(await _context.CartItems
.Where(ci => ci.UserId == _authService.GetUserId()).ToListAsync());
public async Task<ServiceResponse<bool>> AddToCart(CartItem cartItem)
{
cartItem.UserId = _authService.GetUserId();
var sameItem = await _context.CartItems
.FirstOrDefaultAsync(ci => ci.ProductId == cartItem.ProductId &&
ci.ProductTypeId == cartItem.ProductTypeId && ci.UserId == cartItem.UserId);
if (sameItem == null)
_context.CartItems.Add(cartItem);
else
sameItem.Quantity += cartItem.Quantity;
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
public async Task<ServiceResponse<bool>> UpdateQuantity(CartItem cartItem)
{
var dbCartItems = await _context.CartItems
.FirstOrDefaultAsync(ci => ci.ProductId == cartItem.ProductId &&
ci.ProductTypeId == cartItem.ProductTypeId && ci.UserId == _authService.GetUserId());
if(dbCartItems == null)
return new ServiceResponse<bool>
{
Data = false,
Success = false,
Message = "Cart item does not exist."
};
dbCartItems.Quantity = cartItem.Quantity;
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
public async Task<ServiceResponse<bool>> RemoveItemFromCart(int productId, int productTypeId)
{
var dbCartItems = await _context.CartItems
.FirstOrDefaultAsync(ci => ci.ProductId == productId &&
ci.ProductTypeId == productTypeId && ci.UserId == _authService.GetUserId());
if (dbCartItems == null)
return new ServiceResponse<bool>
{
Data = false,
Success = false,
Message = "Cart item does not exist."
};
_context.CartItems.Remove(dbCartItems);
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
}
-
CountAsync
returns count of the list items.
Controller
- BlazorEcommerce.Server/Controllers/CartController.cs
...
public class CartController : ControllerBase
{
...
[HttpPost]
public async Task<ActionResult<ServiceResponse<List<CartProductResponse>>>>
StoreCartItems(List<CartItem> cartItems)
{
var result = await _cartService.StoreCartItems(cartItems);
return Ok(result);
}
[HttpGet("count")]
public async Task<ActionResult<ServiceResponse<int>>> GetCartItemsCount() =>
await _cartService.GetCartItemsCount();
[HttpGet]
public async Task<ActionResult<ServiceResponse<List<CartProductResponse>>>> GetDbCartProducts()
{
var result = await _cartService.GetDbCartProducts();
return Ok(result);
}
[HttpPost("add")]
public async Task<ActionResult<ServiceResponse<bool>>> AddToCart(CartItem cartItem)
{
var result = await _cartService.AddToCart(cartItem);
return Ok(result);
}
[HttpPost("update-quantity")]
public async Task<ActionResult<ServiceResponse<bool>>> UpdateQuantity(CartItem cartItem)
{
var result = await _cartService.UpdateQuantity(cartItem);
return Ok(result);
}
[HttpDelete("{productId}/{productTypeId}")]
public async Task<ActionResult> RemoveItemFromCart(int productId, int productTypeId)
{
var result = await _cartService.RemoveItemFromCart(productId, productTypeId);
return Ok(result);
}
}
Cart Service in Client
Interface
- BlazorEcommerce.Client/Services/CartService/ICartService.cs
public interface ICartService
{
...
Task StoreCartItems(bool emptyLocalCart);
Task GetCartItemsCount();
}
Implement
- BlazorEcommerce.Client/Services/CartService/CartService.cs
public class CartService : ICartService
{
private readonly ILocalStorageService _localStorage;
private readonly HttpClient _http;
private readonly IAuthService _authService;
public CartService(
ILocalStorageService localStorage,
HttpClient http,
IAuthService authService)
{
_localStorage = localStorage;
_http = http;
_authService = authService;
}
public event Action OnChange;
public async Task AddToCart(CartItem cartItem)
{
if (await _authService.IsUserAuthenticated())
await _http.PostAsJsonAsync("api/cart/add", cartItem);
else
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
cart = new List<CartItem>();
var sameItem = cart.Find(x => x.ProductId == cartItem.ProductId &&
x.ProductTypeId == cartItem.ProductTypeId);
if (sameItem == null)
cart.Add(cartItem);
else
sameItem.Quantity += cartItem.Quantity;
await _localStorage.SetItemAsync("cart", cart);
}
await GetCartItemsCount();
}
public async Task<List<CartProductResponse>> GetCartProducts()
{
if (await _authService.IsUserAuthenticated())
{
var response = await _http
.GetFromJsonAsync<ServiceResponse<List<CartProductResponse>>>("api/cart");
return response.Data;
}
else
{
var cartItems = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cartItems == null)
return new List<CartProductResponse>();
var response = await _http.PostAsJsonAsync("api/cart/products", cartItems);
var cartProducts =
await response.Content
.ReadFromJsonAsync<ServiceResponse<List<CartProductResponse>>>();
return cartProducts.Data;
}
}
public async Task RemoveProductFromCart(int productId, int productTypeId)
{
if (await _authService.IsUserAuthenticated())
await _http.DeleteAsync($"api/cart/{productId}/{productTypeId}");
else
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
return;
var cartItem = cart.Find(x => x.ProductId == productId
&& x.ProductTypeId == productTypeId);
if (cartItem != null)
{
cart.Remove(cartItem);
await _localStorage.SetItemAsync("cart", cart);
}
}
await GetCartItemsCount();
}
public async Task UpdateQuantity(CartProductResponse product)
{
if (await _authService.IsUserAuthenticated())
{
var request = new CartItem
{
ProductId = product.ProductId,
Quantity = product.Quantity,
ProductTypeId = product.ProductTypeId
};
await _http.PostAsJsonAsync("api/cart/update-quantity", request);
}
else
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
return;
var cartItem = cart.Find(x => x.ProductId == product.ProductId
&& x.ProductTypeId == product.ProductTypeId);
if (cartItem != null)
{
cartItem.Quantity = product.Quantity;
await _localStorage.SetItemAsync("cart", cart);
}
}
}
public async Task StoreCartItems(bool emptyLocalCart = false)
{
var localCart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (localCart == null)
return;
await _http.PostAsJsonAsync("api/cart", localCart);
if (emptyLocalCart)
await _localStorage.RemoveItemAsync("cart");
}
public async Task GetCartItemsCount()
{
if (await _authService.IsUserAuthenticated())
{
var result = await _http.GetFromJsonAsync<ServiceResponse<int>>("api/cart/count");
var count = result.Data;
await _localStorage.SetItemAsync<int>("cartItemsCount", count);
}
else
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
await _localStorage.SetItemAsync<int>("cartItemsCount", cart != null ? cart.Count : 0);
}
OnChange.Invoke();
}
}
Order
Models
- BlazorEcommerce.Shared/Order.cs
public class Order
{
public int Id { get; set; }
public int UserId { get; set; }
public DateTime OrderDate { get; set; } = DateTime.Now;
[Column(TypeName = "decimal(18,2)")]
public decimal TotalPrice { get; set; }
public List<OrderItem> OrderItems { get; set; }
}
- BlazorEcommerce.Shared/OrderDetailsProductResponse.cs
public class OrderDetailsProductResponse
{
public int ProductId { get; set; }
public string Title { get; set; }
public string ProductType { get; set; }
public string ImageUrl { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
}
- BlazorEcommerce.Shared/OrderDetailsResponse.cs
public class OrderDetailsResponse
{
public DateTime OrderDate { get; set; }
public decimal TotalPrice { get; set; }
public List<OrderDetailsProductResponse> Products { get; set; }
}
- BlazorEcommerce.Shared/OrderItem.cs
public class OrderItem
{
public Order Order { get; set; }
public int OrderId { get; set; }
public Product Product { get; set; }
public int ProductId { get; set; }
public ProductType ProductType { get; set; }
public int ProductTypeId { get; set; }
public int Quantity { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal TotalPrice { get; set; }
}
- BlazorEcommerce.Shared/OrderOverviewResponse.cs
public class OrderOverviewResponse
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalPrice { get; set; }
public string Product { get; set; }
public string ProductImageUrl { get; set; }
}
Set
- BlazorEcommerce.Server/Data/DataContext.cs
public class DataContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.OrderId, oi.ProductId, oi.ProductTypeId });
...
}
...
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
}
Database
- Type
dotnet ef migrations add Orders
anddotnet ef database update
Order Service in Server
Interface
- BlazorEcommerce.Server/Services/OrderService/IOrderService.cs
public interface IOrderService
{
Task<ServiceResponse<bool>> PlaceOrder();
Task<ServiceResponse<List<OrderOverviewResponse>>> GetOrders();
Task<ServiceResponse<OrderDetailsResponse>> GetOrderDetails(int orderId);
}
Implement
- BlazorEcommerce.Server/Services/OrderService/OrderService.cs
public class OrderService : IOrderService
{
private readonly DataContext _context;
private readonly IAuthService _authService;
private readonly ICartService _cartService;
public OrderService(
DataContext context,
IAuthService authService,
ICartService cartService)
{
_context = context;
_authService = authService;
_cartService = cartService;
}
public async Task<ServiceResponse<bool>> PlaceOrder()
{
var products = (await _cartService.GetDbCartProducts()).Data;
decimal totalPrice = 0;
products.ForEach(product => totalPrice += product.Price * product.Quantity);
var orderItems = new List<OrderItem>();
products.ForEach(product => orderItems.Add(new OrderItem
{
ProductId = product.ProductId,
ProductTypeId = product.ProductTypeId,
Quantity = product.Quantity,
TotalPrice = product.Price * product.Quantity
}));
var order = new Order
{
UserId = _authService.GetUserId(),
OrderDate = DateTime.Now,
TotalPrice = totalPrice,
OrderItems = orderItems
};
_context.Add(order);
_context.CartItems.RemoveRange(_context.CartItems
.Where(ci => ci.UserId == _authService.GetUserId()));
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
public async Task<ServiceResponse<List<OrderOverviewResponse>>> GetOrders()
{
var response = new ServiceResponse<List<OrderOverviewResponse>>();
var order = await _context.Orders
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Where(o => o.UserId == _authService.GetUserId())
.OrderByDescending(o => o.OrderDate)
.ToListAsync();
var orderResponse = new List<OrderOverviewResponse>();
order.ForEach(o => orderResponse.Add(new OrderOverviewResponse
{
Id = o.Id,
OrderDate = o.OrderDate,
TotalPrice = o.TotalPrice,
Product = o.OrderItems.Count > 1 ?
$"{o.OrderItems.First().Product.Title} and" +
$"{o.OrderItems.Count - 1} more..." :
o.OrderItems.First().Product.Title,
ProductImageUrl = o.OrderItems.First().Product.ImageUrl
}));
response.Data = orderResponse;
return response;
}
public async Task<ServiceResponse<OrderDetailsResponse>> GetOrderDetails(int orderId)
{
var response = new ServiceResponse<OrderDetailsResponse>();
var order = await _context.Orders
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.ProductType)
.Where(o => o.UserId == _authService.GetUserId() && o.Id == orderId)
.OrderByDescending(o => o.OrderDate)
.FirstOrDefaultAsync();
if(order == null)
{
response.Success = false;
response.Message = "Order not found.";
return response;
}
var orderDetailsResponse = new OrderDetailsResponse
{
OrderDate = order.OrderDate,
TotalPrice = order.TotalPrice,
Products = new List<OrderDetailsProductResponse>()
};
order.OrderItems.ForEach(item =>
orderDetailsResponse.Products.Add(new OrderDetailsProductResponse
{
ProductId = item.ProductId,
ImageUrl = item.Product.ImageUrl,
ProductType = item.ProductType.Name,
Quantity = item.Quantity,
Title = item.Product.Title,
TotalPrice = item.TotalPrice
}));
response.Data = orderDetailsResponse;
return response;
}
}
Controller
- BlazorEcommerce.Server/Controllers/OrderController.cs
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<ActionResult<ServiceResponse<bool>>> PlaceOrder()
{
var result = await _orderService.PlaceOrder();
return Ok(result);
}
[HttpGet]
public async Task<ActionResult<ServiceResponse<List<OrderOverviewResponse>>>> GetOrders()
{
var result = await _orderService.GetOrders();
return Ok(result);
}
[HttpGet("{orderId}")]
public async Task<ActionResult<ServiceResponse<OrderDetailsResponse>>> GetOrderDetails
(int orderId)
{
var result = await _orderService.GetOrderDetails(orderId);
return Ok(result);
}
}
Set
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<IOrderService, OrderService>();
...
Order Service in Client
Interface
- BlazorEcommerce.Client/Services/OrderService/IOrderService.cs
public interface IOrderService
{
Task PlaceOrder();
Task<List<OrderOverviewResponse>> GetOrders();
Task<OrderDetailsResponse> GetOrderDetails(int orderId);
}
Implement
- BlazorEcommerce.Client/Services/OrderService/OrderService.cs
public class OrderService : IOrderService
{
private readonly HttpClient _http;
private readonly AuthenticationStateProvider _authStateProvider;
private readonly NavigationManager _navigationManager;
public OrderService(
HttpClient http,
AuthenticationStateProvider authStateProvider,
NavigationManager navigationManager)
{
_http = http;
_authStateProvider = authStateProvider;
_navigationManager = navigationManager;
}
private async Task<bool> IsUserAuthenticated()
=> (await _authStateProvider.GetAuthenticationStateAsync())
.User.Identity.IsAuthenticated;
public async Task PlaceOrder()
{
if (await IsUserAuthenticated())
await _http.PostAsync("api/order", null);
else
_navigationManager.NavigateTo("login");
}
public async Task<List<OrderOverviewResponse>> GetOrders()
{
var result = await _http
.GetFromJsonAsync<ServiceResponse<List<OrderOverviewResponse>>>("api/order");
return result.Data;
}
public async Task<OrderDetailsResponse> GetOrderDetails(int orderId)
{
var result = await _http
.GetFromJsonAsync<ServiceResponse<OrderDetailsResponse>>($"api/order/{orderId}");
return result.Data;
}
}
Set
- BlazorEcommerce.Client/Program.cs
...
builder.Services.AddScoped<IOrderService, OrderService>();
...
View
Cart
- BlazorEcommerce.Client/Shared/UserButton.razor
...
<AuthorizeView>
<Authorized>
<a href="profile" class="dropdown-item">Profile</a>
<a href="orders" class="dropdown-item">Orders</a>
<hr/>
<button class="dropdown-item" @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="login?returnUrl=@NavigationManager.ToBaseRelativePath(NavigationManager.Uri)"
class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
</AuthorizeView>
...
- BlazorEcommerce.Client/Shared/UserButton.razor.cs
public partial class UserButton
{
...
[Inject]
public ICartService CartService { get; set; }
...
private async Task Logout()
{
await LocalStorage.RemoveItemAsync("authToken");
await CartService.GetCartItemsCount();
await AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo("");
}
}
- BlazorEcommerce.Client/Shared/CartCounter.razor.cs
public partial class CartCounter : IDisposable
{
...
private int GetCartItemCount()
{
var count = LocalStorage.GetItem<int>("cartItemsCount");
return count;
}
...
}
- BlazorEcommerce.Client/Pages/Login.razor.cs
public partial class Login
{
...
[Inject]
public ICartService CartService { get; set; }
...
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if(result.Success)
{
errorMessage = string.Empty;
await LocalStorage.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.GetAuthenticationStateAsync();
await CartService.StoreCartItems(true);
await CartService.GetCartItemsCount();
NavigationManager.NavigateTo(returnUrl);
}
else
errorMessage = result.Message;
}
}
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class Login
{
...
private async Task LoadCart()
{
await CartService.GetCartItemsCount();
cartProducts = await CartService.GetCartProducts();
if (!cartProducts.Any())
message = "Your cart is empty.";
}
}
Order
- BlazorEcommerce.Client/Pages/Cart.razor
...
else if(orderPlaced)
{
<span>Thank you for your order! You can check your orders <a href="orders">here</a>.</span>
}
else
{
...
<button @onclick="PlaceOrder" class="btn alert-success float-end mt-1">Place Order</button>
}
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class Login
{
...
[Inject]
public IOrderService OrderService { get; set; }
...
bool orderPlaced = false;
protected override async Task OnInitializedAsync()
{
orderPlaced = false;
await LoadCart();
}
private async Task PlaceOrder()
{
await OrderService.PlaceOrder();
await CartService.GetCartItemsCount();
orderPlaced = true;
}
}
- BlazorEcommerce.Client/Pages/Order.razor
@page "/orders"
<h3>Orders</h3>
@if(orders == null)
{
<span>Loading your orders...</span>
}
else if(orders.Count <= 0)
{
<span>You have no orders, yet.</span>
}
else
{
foreach(var order in orders)
{
<div class="container">
<div class="image-wrapper">
<img src="@order.ProductImageUrl" class="image"/>
</div>
<div class="details">
<h4>@order.Product</h4>
<span>@order.OrderDate</span><br/>
<a href="orders/@order.Id">Show more...</a>
</div>
<div class="order-price">$@order.TotalPrice</div>
</div>
}
}
- BlazorEcommerce.Client/Pages/Order.razor.cs
public partial class Orders
{
[Inject]
public IOrderService OrderService { get; set; }
List<OrderOverviewResponse> orders = null;
protected override async Task OnInitializedAsync()
{
orders = await OrderService.GetOrders();
}
}
- BlazorEcommerce.Client/Pages/Order.razor.css
.container {
display: flex;
padding: 6px;
border: 1px solid;
border-color: lightgray;
border-radius: 6px;
margin-bottom: 10px;
}
.image-wrapper {
width: 150px;
text-align: center;
}
.image {
max-height: 150px;
max-width: 150px;
padding: 6px;
}
.details {
flex-grow: 1;
padding: 6px;
}
.order-price {
font-weight: 600;
font-size: 1.2em;
text-align: right;
}
- BlazorEcommerce.Client/Pages/OrderDetails.razor
@page "/orders/{orderId:int}"
@if(order == null)
{
<span>Loading order...</span>
}
else
{
<h3>Order from @order.OrderDate</h3>
<div>
@foreach(var product in order.Products)
{
<div class="container">
<div class="image-wrapper">
<img src="@product.ImageUrl" class="image"/>
</div>
</div>
<div class="name">
<h5><a href="/product/@product.ProductId">@product.Title</a></h5>
<span>@product.ProductType</span><br/>
<span>Quantity: @product.Quantity</span>
</div>
<div class="product-price">$@product.TotalPrice</div>
}
<div class="product-price">
Total: $@order.TotalPrice
</div>
</div>
}
- BlazorEcommerce.Client/Pages/OrderDetails.razor.cs
public partial class OrderDetails
{
[Parameter]
public int OrderId { get; set; }
[Inject]
public IOrderService OrderService { get; set; }
OrderDetailsResponse order = null;
protected override async Task OnInitializedAsync()
{
order = await OrderService.GetOrderDetails(OrderId);
}
}
- BlazorEcommerce.Client/Pages/OrderDetails.razor.css
.container {
display: flex;
padding: 6px;
}
.image-wrapper {
width: 150px;
text-align: center;
}
.image {
max-height: 150px;
max-width: 150px;
padding: 6px;
}
.name {
flex-grow: 1;
padding: 6px;
}
.product-price {
font-weight: 600;
text-align: right;
}
Run
Cart


Order


Payment with Webhook
- A webhook in web development is a method of augmenting or altering the behavior of a web page or web application with custom callbacks.
Stripe
- It offers payment processing software and application programming interfaces (APIs) for e-commerce websites and mobile.
- Please read Stripe Documents, if you want to get more information.
Account
- Create account ot Stripe.

API Key
- Create your secret key from Stripe.

Stripe CLI
- Download Stripe-Cli.
- Login in Stripe with
stripe login
.


Package in Server
- Download
Stripe.net
in Server.

Payment Service in Server
Get User Email From Auth Service
- BlazorEcommerce.Server/Services/AuthService/IAuthService.cs
public interface IAuthService
{
...
string GetUserEmail();
Task<User> GetUserByEmail(string email);
}
- BlazorEcommerce.Server/Services/AuthService/AuthService.cs
public class AuthService : IAuthService
{
...
public string GetUserEmail()
=> _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name);
public async Task<User> GetUserByEmail(string email)
=> await _context.Users.FirstOrDefaultAsync(u => u.Email.Equals(email));
...
}
When you checkout, Get Cart Products from Database
- BlazorEcommerce.Server/Services/CartService/ICartService.cs
public interface ICartService
{
...
Task<ServiceResponse<List<CartProductResponse>>> GetDbCartProducts(int? userId = null);
...
}
- BlazorEcommerce.Server/Services/CartService/CartService.cs
public class CartService : ICartService
{
...
public async Task<ServiceResponse<List<CartProductResponse>>> GetDbCartProducts
(int? userId = null)
{
if(userId == null)
userId = _authService.GetUserId();
return await GetCartProducts(await _context.CartItems
.Where(ci => ci.UserId == userId).ToListAsync());
}
...
}
get Orders when you checkout
- BlazorEcommerce.Server/Services/OrderService/IOrderService.cs
public interface IOrderService
{
Task<ServiceResponse<bool>> PlaceOrder(int userId);
...
}
- BlazorEcommerce.Server/Services/OrderService/OrderService.cs
public class CartService : ICartService
{
...
public async Task<ServiceResponse<bool>> PlaceOrder(int userId)
{
var products = (await _cartService.GetDbCartProducts(userId)).Data;
decimal totalPrice = 0;
products.ForEach(product => totalPrice += product.Price * product.Quantity);
var orderItems = new List<OrderItem>();
products.ForEach(product => orderItems.Add(new OrderItem
{
ProductId = product.ProductId,
ProductTypeId = product.ProductTypeId,
Quantity = product.Quantity,
TotalPrice = product.Price * product.Quantity
}));
var order = new Order
{
UserId = userId,
OrderDate = DateTime.Now,
TotalPrice = totalPrice,
OrderItems = orderItems
};
_context.Add(order);
_context.CartItems.RemoveRange(_context.CartItems
.Where(ci => ci.UserId == userId));
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
...
}
- BlazorEcommerce.Server/Controllers/OrderController.cs
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpGet]
public async Task<ActionResult<ServiceResponse<List<OrderOverviewResponse>>>> GetOrders()
{
var result = await _orderService.GetOrders();
return Ok(result);
}
[HttpGet("{orderId}")]
public async Task<ActionResult<ServiceResponse<OrderDetailsResponse>>> GetOrderDetails
(int orderId)
{
var result = await _orderService.GetOrderDetails(orderId);
return Ok(result);
}
}
Payment Service
- BlazorEcommerce.Server/Services/PaymentService/IPaymentService.cs
public interface IPaymentService
{
Task<Session> CreateCheckoutSession();
Task<ServiceResponse<bool>> FulfillOrder(HttpRequest request);
}
- BlazorEcommerce.Server/Services/PaymentService/PaymentService.cs
public class PaymentService : IPaymentService
{
private readonly IAuthService _authService;
private readonly ICartService _cartService;
private readonly IOrderService _orderService;
const string secret = "your secret key";
public PaymentService(
IAuthService authService,
ICartService cartService,
IOrderService orderService)
{
StripeConfiguration.ApiKey = "your api key";
_authService = authService;
_cartService = cartService;
_orderService = orderService;
}
public async Task<Session> CreateCheckoutSession()
{
var products = (await _cartService.GetDbCartProducts()).Data;
var lineItems = new List<SessionLineItemOptions>();
products.ForEach(product => lineItems.Add(new SessionLineItemOptions
{
PriceData = new SessionLineItemPriceDataOptions
{
UnitAmountDecimal = product.Price * 100,
Currency = "usd",
ProductData = new SessionLineItemPriceDataProductDataOptions
{
Name = product.Title,
Images = new List<string> { product.ImageUrl }
}
},
Quantity = product.Quantity
}));
var options = new SessionCreateOptions
{
CustomerEmail = _authService.GetUserEmail(),
ShippingAddressCollection =
new SessionShippingAddressCollectionOptions
{
AllowedCountries = new List<string> { "US" }
},
PaymentMethodTypes = new List<string> { "card" },
LineItems = lineItems,
Mode = "payment",
SuccessUrl = "your local host url/order-success",
CancelUrl = "your local host url/cart"
};
var service = new SessionService();
Session session = service.Create(options);
return session;
}
public async Task<ServiceResponse<bool>> FulfillOrder(HttpRequest request)
{
var json = await new StreamReader(request.Body).ReadToEndAsync();
try
{
var stripeEvent = EventUtility.ConstructEvent(
json,
request.Headers["Stripe-Signature"],
secret);
if(stripeEvent.Type == Events.CheckoutSessionCompleted)
{
var session = stripeEvent.Data.Object as Session;
var user = await _authService.GetUserByEmail(session.CustomerEmail);
await _orderService.PlaceOrder(user.Id);
}
return new ServiceResponse<bool> { Data = true };
}
catch (StripeException e)
{
return new ServiceResponse<bool> { Data = false, Success = false, Message = e.Message };
}
}
}
- BlazorEcommerce.Server/Controllers/PaymentController.cs
[Route("api/[controller]")]
[ApiController]
public class PaymentController : ControllerBase
{
private readonly IPaymentService _paymentService;
public PaymentController(IPaymentService paymentService)
{
_paymentService = paymentService;
}
[HttpPost("checkout"), Authorize]
public async Task<ActionResult<string>> CreateCheckoutSession()
{
var session = await _paymentService.CreateCheckoutSession();
return Ok(session.Url);
}
[HttpPost]
public async Task<ActionResult<ServiceResponse<string>>> FulfillOrder()
{
var response = await _paymentService.FulfillOrder(Request);
if(!response.Success)
return BadRequest(response.Message);
return Ok(response);
}
}
Set
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<IPaymentService, PaymentService>();
...
Refactoring Order Service in Client
- BlazorEcommerce.Client/Services/OrderService/IOrderService.cs
public interface IOrderService
{
Task<string> PlaceOrder();
...
}
- BlazorEcommerce.Client/Services/OrderService/OrderService.cs
public class OrderService : IOrderService
{
...
public async Task<string> PlaceOrder()
{
if (await IsUserAuthenticated())
{
var result = await _http.PostAsync("api/payment/checkout", null);
var url = await result.Content.ReadAsStringAsync();
return url;
}
else
return "login";
}
...
}
View
- BlazorEcommerce.Client/Pages/Cart.razor
@page "/cart"
<h3>Shopping Cart</h3>
@if (!cartProducts.Any())
{
<span>@message</span>
}
else
{
<div>
@foreach(var product in cartProducts)
{
<div class="container">
<div class="image-wrapper">
<img src="@product.ImageUrl" class="image"/>
</div>
<div class="name">
<h5><a href="/product/@product.ProductId">@product.Title</a></h5>
<span>@product.ProductType</span><br/>
<input type="number"
value="@product.Quantity"
@onchange="@((ChangeEventArgs e) => UpdateQuantity(e, product))"
class="form-control input-quantity"
min="1"/>
<button class="btn-delete"
@onclick="@(()
=> RemoveProductFromCart(product.ProductId, product.ProductTypeId))">
Delete</button>
</div>
<div class="cart-product-price">$@(product.Price * product.Quantity)</div>
</div>
}
<div class="cart-product-price">
Total (@cartProducts.Count): $@cartProducts.Sum(product =>
@product.Price * product.Quantity)
</div>
</div>
<button @onclick="PlaceOrder" class="btn alert-success float-end mt-1">Checkout</button>
}
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class Cart
{
...
[Inject]
public NavigationManager NavigationManager { get; set; }
...
protected override async Task OnInitializedAsync()
=> await LoadCart();
...
private async Task PlaceOrder()
{
string url = await OrderService.PlaceOrder();
NavigationManager.NavigateTo(url);
}
}
- BlazorEcommerce.Client/Pages/OrderSuccess.razor
@page "/order-success"
<h3>Thank you!</h3>
<span>Thank you for your order! You can check your orders <a href="orders">here</a>.</span>
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class OrderSucess
{
[Inject]
public ICartService CartService { get; set; }
protected override async Task OnInitializedAsync()
=> await CartService.GetCartItemsCount();
}
Run
Payment


Address
Models
- BlazorEcommerce.Shared/Address.cs
public class Address
{
public int Id { get; set; }
public int UserId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string State { get; set; } = string.Empty;
public string Zip { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}
- BlazorEcommerce.Shared/User.cs
public class User
{
...
public Address Address { get; set; }
}
Set
- BlazorEcommerce.Shared/Program.cs
public class DataContext : DbContext
{
...
public DbSet<Address> Addresses { get; set; }
}
Database
- Type
dotnet ef migrations add UserAddress
anddotnet ef database update
.
Address Service in Server
Interface
- BlazorEcommerce.Server/Services/AddressService/IAddressService.cs
public interface IAddressService
{
Task<ServiceResponse<Address>> GetAddress();
Task<ServiceResponse<Address>> AddOrUpdateAddress(Address address);
}
Implement
- BlazorEcommerce.Server/Services/AddressService/AddressService.cs
public class AddressService : IAddressService
{
private readonly DataContext _context;
private readonly IAuthService _authService;
public AddressService(DataContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
public async Task<ServiceResponse<Address>> AddOrUpdateAddress(Address address)
{
var response = new ServiceResponse<Address>();
var dbAddress = (await GetAddress()).Data;
if(dbAddress == null)
{
address.UserId = _authService.GetUserId();
_context.Addresses.Add(address);
response.Data = address;
}
else
{
dbAddress.FirstName = address.FirstName;
dbAddress.LastName = address.LastName;
dbAddress.Street = address.Street;
dbAddress.City = address.City;
dbAddress.State = address.State;
dbAddress.Zip = address.Zip;
dbAddress.Country = address.Country;
response.Data = dbAddress;
}
await _context.SaveChangesAsync();
return response;
}
public async Task<ServiceResponse<Address>> GetAddress()
{
int userId = _authService.GetUserId();
var address = await _context.Addresses
.FirstOrDefaultAsync(a => a.UserId == userId);
return new ServiceResponse<Address> { Data = address };
}
}
Controller
- BlazorEcommerce.Server/Controllers/AddressController.cs
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class AddressController : ControllerBase
{
private readonly IAddressService _addressService;
public AddressController(IAddressService addressService)
{
_addressService = addressService;
}
[HttpGet]
public async Task<ActionResult<ServiceResponse<Address>>> GetAddress()
=> await _addressService.GetAddress();
[HttpPost]
public async Task<ActionResult<ServiceResponse<Address>>> AddOrUpdateAddress(Address address)
=> await _addressService.AddOrUpdateAddress(address);
}
Set
- BlazorEcommerce.Server/Program.cs
...
builder.Services.AddScoped<IAddressService, AddressService>();
...
Address Service in Client
Interface
- BlazorEcommerce.Client/Services/AddressService/IAddressService.cs
public interface IAddressService
{
Task<Address> GetAddress();
Task<Address> AddOrUpdateAddress(Address address);
}
Implement
- BlazorEcommerce.Client/Services/AddressService/AddressService.cs
public class AddressService : IAddressService
{
private readonly HttpClient _http;
public AddressService(HttpClient http)
{
_http = http;
}
public async Task<Address> AddOrUpdateAddress(Address address)
{
var response = await _http.PostAsJsonAsync("api/address", address);
return response.Content.ReadFromJsonAsync<ServiceResponse<Address>>().Result.Data;
}
public async Task<Address> GetAddress()
{
var response = await _http.GetFromJsonAsync<ServiceResponse<Address>>("api/address");
return response.Data;
}
}
Set
- BlazorEcommerce.Client/Program.cs
...
builder.Services.AddScoped<IAddressService, AddressService>();
...
View
- BlazorEcommerce.Client/Shared/AddressForm.razor
@if (address == null)
{
<span>
You haven't specified a delivery address, yet.
<button class="btn" @onclick="InitAddress">Add an address?</button>
</span>
}
else if (!editAddress)
{
<p>
<span>@address.FirstName @address.LastName</span><br/>
<span>@address.Street</span><br/>
<span>@address.City @address.State @address.Zip</span><br/>
<span>@address.Country</span><br/>
</p>
<button class="btn btn-primary" @onclick="EditAddress">Edit</button>
}
else
{
<EditForm Model="address" OnSubmit="SubmitAddress">
<div class="mb-3">
<label for="firstname">First Name</label>
<InputText id="firstname" @bind-Value="address.FirstName" class="form-control" />
</div>
<div class="mb-3">
<label for="lastname">Last Name</label>
<InputText id="lastname" @bind-Value="address.LastName" class="form-control" />
</div>
<div class="mb-3">
<label for="street">Street</label>
<InputText id="street" @bind-Value="address.Street" class="form-control" />
</div>
<div class="mb-3">
<label for="city">City</label>
<InputText id="city" @bind-Value="address.City" class="form-control" />
</div>
<div class="mb-3">
<label for="state">State</label>
<InputText id="state" @bind-Value="address.State" class="form-control" />
</div>
<div class="mb-3">
<label for="zip">Zip/Postal Code</label>
<InputText id="zip" @bind-Value="address.Zip" class="form-control" />
</div>
<div class="mb-3">
<label for="country">Country</label>
<InputText id="country" @bind-Value="address.Country" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Save</button>
</EditForm>
}
- BlazorEcommerce.Client/Shared/AddressForm.razor.cs
public partial class AddressForm
{
[Inject]
public IAddressService AddressService { get; set; }
Address address = null;
bool editAddress = false;
protected override async Task OnInitializedAsync()
=> address = await AddressService.GetAddress();
private async Task SubmitAddress()
{
editAddress = false;
address = await AddressService.AddOrUpdateAddress(address);
}
private void InitAddress()
{
address = new Address();
editAddress = true;
}
private void EditAddress()
=> editAddress = true;
}
- BlazorEcommerce.Client/Pages/Cart.razor
...
@if(isAuthenticated)
{
<div>
<h5>Delivery Address</h5>
<AddressForm />
</div>
}
...
- BlazorEcommerce.Client/Pages/Cart.razor.cs
public partial class Cart
{
...
[Inject]
public IAuthService AuthService { get; set; }
...
bool isAuthenticated = false;
protected override async Task OnInitializedAsync()
{
isAuthenticated = await AuthService.IsUserAuthenticated();
await LoadCart();
}
...
}
- BlazorEcommerce.Client/Pages/Profile.razor
...
<h5>Delivery Address</h5>
<AddressForm />
<p></p>
...
Run
Profile



Cart
