diff --git a/VizhTalk.BuildingBlocks.Application/Exceptions/LanguageTitleAlreadyExistsException.cs b/VizhTalk.BuildingBlocks.Application/Exceptions/LanguageTitleAlreadyExistsException.cs deleted file mode 100644 index 4e7aa88..0000000 --- a/VizhTalk.BuildingBlocks.Application/Exceptions/LanguageTitleAlreadyExistsException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace VizhTalk.BuildingBlocks.Application.Exceptions -{ - public class LanguageTitleAlreadyExistsException : BaseException - { - public LanguageTitleAlreadyExistsException() : base("title already exists") - { - } - } -} diff --git a/VizhTalk.BuildingBlocks.Application/IUnitOfWorkBase.cs b/VizhTalk.BuildingBlocks.Application/IUnitOfWorkBase.cs index 413777e..d59f0c5 100644 --- a/VizhTalk.BuildingBlocks.Application/IUnitOfWorkBase.cs +++ b/VizhTalk.BuildingBlocks.Application/IUnitOfWorkBase.cs @@ -1,7 +1,10 @@ -namespace VizhTalk.BuildingBlocks.Application; +using System.Transactions; + +namespace VizhTalk.BuildingBlocks.Application; public interface IUnitOfWorkBase : IDisposable { - Task CommitAsync( - CancellationToken cancellationToken = default); + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); + Task CommitAsync(CancellationToken cancellationToken = default); } diff --git a/VizhTalk.BuildingBlocks.Domain/Contract/IGuardException.cs b/VizhTalk.BuildingBlocks.Domain/Contract/IGuardException.cs index d940701..edd77c6 100644 --- a/VizhTalk.BuildingBlocks.Domain/Contract/IGuardException.cs +++ b/VizhTalk.BuildingBlocks.Domain/Contract/IGuardException.cs @@ -8,5 +8,6 @@ namespace VizhTalk.BuildingBlocks.Domain.Contract { public interface IGuardException { + public string Message { get; } } } diff --git a/VizhTalk.BuildingBlocks.Domain/Exceptions/ValueObjectValidationException.cs b/VizhTalk.BuildingBlocks.Domain/Exceptions/ValueObjectValidationException.cs deleted file mode 100644 index 9d5d7a8..0000000 --- a/VizhTalk.BuildingBlocks.Domain/Exceptions/ValueObjectValidationException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace VizhTalk.BuildingBlocks.Domain.Exceptions; - -public class ValueObjectValidationException : Exception -{ - public ValueObjectValidationException(string message) - :base(message) - { - } -} diff --git a/VizhTalk.BuildingBlocks.Infrastructure/RepositoryBase.cs b/VizhTalk.BuildingBlocks.Infrastructure/RepositoryBase.cs index 55a911a..44071af 100644 --- a/VizhTalk.BuildingBlocks.Infrastructure/RepositoryBase.cs +++ b/VizhTalk.BuildingBlocks.Infrastructure/RepositoryBase.cs @@ -21,7 +21,7 @@ namespace VizhTalk.BuildingBlocks.Infrastructure this._dbContext = dbContext; } - + protected DbSet Entity => _dbContext.Set(); public async Task AddAsync(T entity, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/VizhTalk.BuildingBlocks.Infrastructure/UnitOfWorkBase.cs b/VizhTalk.BuildingBlocks.Infrastructure/UnitOfWorkBase.cs index 896c595..72acca8 100644 --- a/VizhTalk.BuildingBlocks.Infrastructure/UnitOfWorkBase.cs +++ b/VizhTalk.BuildingBlocks.Infrastructure/UnitOfWorkBase.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Transactions; using VizhTalk.BuildingBlocks.Application; using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching; @@ -17,12 +19,24 @@ public class UnitOfWorkBase : IUnitOfWorkBase this._domainEventsDispatcher = domainEventsDispatcher; } + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + using var tran = await _context.Database.BeginTransactionAsync(cancellationToken); + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + => await _context.Database.RollbackTransactionAsync(cancellationToken); + public async Task CommitAsync( CancellationToken cancellationToken = default) { await this._domainEventsDispatcher.DispatchEventsAsync(); - return await _context.SaveChangesAsync(cancellationToken); + var num = await _context.SaveChangesAsync(cancellationToken); + if (_context.Database.CurrentTransaction != null) + await _context.Database.CommitTransactionAsync(cancellationToken); + + return num; } public void Dispose() @@ -30,4 +44,6 @@ public class UnitOfWorkBase : IUnitOfWorkBase GC.SuppressFinalize(this); _context.Dispose(); } + + } \ No newline at end of file diff --git a/VizhTalk.Grpc.CDN/DTOs.proto b/VizhTalk.Grpc.CDN/DTOs.proto index 13f9f14..56ede94 100644 --- a/VizhTalk.Grpc.CDN/DTOs.proto +++ b/VizhTalk.Grpc.CDN/DTOs.proto @@ -10,6 +10,8 @@ message Id{ message UploadFileDTO{ Id mediaGroupMediaId = 1; + string contentType = 2; + int32 size = 3; } message MediaGroupDTO{ Id id = 1; diff --git a/VizhTalk.Learning.Application/CommandHandlers/LanguageCommandHandler.cs b/VizhTalk.Learning.Application/CommandHandlers/LanguageCommandHandler.cs index bff4cd2..cea7655 100644 --- a/VizhTalk.Learning.Application/CommandHandlers/LanguageCommandHandler.cs +++ b/VizhTalk.Learning.Application/CommandHandlers/LanguageCommandHandler.cs @@ -1,4 +1,6 @@ using Mapster; +using System.ComponentModel.DataAnnotations; +using System.Net.Sockets; using VizhTalk.BuildingBlocks.Application.Configuration.Commnad; using VizhTalk.BuildingBlocks.Application.Exceptions; using VizhTalk.BuildingBlocks.Domain.Contract; @@ -12,23 +14,55 @@ using VizhTalk.Learning.Domain.ValueObjects; namespace VizhTalk.Learning.Application.CommandHandlers; -public class CreateLanguageCommandHandler(IUnitOfWork unitOfWork, CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler +public class CreateLanguageCommandHandler(IUnitOfWork unitOfWork, + CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler { public async Task Handle(CreateLanguageCommand request, CancellationToken cancellationToken) { - var title = request.Title.Trim(); - if (await unitOfWork.Language.Exists(x => x.Title == title)) - throw new LanguageTitleAlreadyExistsException(); - + await unitOfWork.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + try + { + var title = request.Title.Trim(); + if (await unitOfWork.Language.Exists(x => x.Title == title)) + throw new LanguageTitleAlreadyExistsException(); - var language = Language.CreateNew(new LanguageTitle(title), null); - await unitOfWork.Language.AddAsync(language, cancellationToken); - await unitOfWork.SaveChangeAsync(cancellationToken); - return language.Adapt(); + var fileGroup = await unitOfWork.FileGroup.GetByFileGroupKey(FileGroupKey.IconLanguage); + if (fileGroup == null) + { + var group = await cdnClient.CreateMediaGroupAsync(new CreateMediaGroupCommand() + { + IsSecure = false, + Key = FileGroupKey.IconLanguage.Value, + Title = nameof(FileGroupKey.IconLanguage) + }, cancellationToken: cancellationToken); + + fileGroup = FileGroup.CreateNew(FileGroupKey.IconLanguage, new FileGroupTitle(nameof(FileGroupKey.IconLanguage)), false, group.Id); + await unitOfWork.FileGroup.AddAsync(fileGroup, cancellationToken); + } + var upload = await cdnClient.UploadFileAsync(new UploadFileCommand() + { + File = request.Icon, + FileName = title, + MediaGroupId = fileGroup.MediaGroupId + }, cancellationToken: cancellationToken); + + var language = Language.CreateNew(new LanguageTitle(title)); + await unitOfWork.Language.AddAsync(language, cancellationToken); + await unitOfWork.SaveChangeAsync(cancellationToken); + var media = Media.CreateNew(title, upload.ContentType, upload.Size, new EntityType(language.Id.Value.ToString(), EntityTypeEnum.Language)); + await unitOfWork.Media.AddAsync(media, cancellationToken); + language.SetIcon(media); + return language.Adapt(); + } + catch (Exception ex) + { + await unitOfWork.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); + throw; + } } } -public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork) : ICommandHandler +public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork, CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler { public async Task Handle(UpdateLanguageCommand request, CancellationToken cancellationToken) { @@ -37,7 +71,21 @@ public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork) : ICommandHand throw new LanguageNotFoundException(); language.SetTitle(new LanguageTitle(request.Title)); + if(request.Icon != null) + { + var fileGroup = await unitOfWork.FileGroup.GetByFileGroupKey(FileGroupKey.IconLanguage); + var upload = await cdnClient.UploadFileAsync(new UploadFileCommand() + { + File = request.Icon, + FileName = request.Title, + MediaGroupId = fileGroup.MediaGroupId + }, cancellationToken: cancellationToken); + + var media = Media.CreateNew(request.Title, upload.ContentType, upload.Size, new EntityType(language.Id.Value.ToString(), EntityTypeEnum.Language)); + + language.SetIcon(media); + } return language.Adapt(); } } diff --git a/VizhTalk.Learning.Application/Exceptions/LanguageTitleAlreadyExistsException.cs b/VizhTalk.Learning.Application/Exceptions/LanguageTitleAlreadyExistsException.cs new file mode 100644 index 0000000..7b13ffb --- /dev/null +++ b/VizhTalk.Learning.Application/Exceptions/LanguageTitleAlreadyExistsException.cs @@ -0,0 +1,10 @@ +using VizhTalk.BuildingBlocks.Application.Exceptions; + +namespace VizhTalk.Learning.Application.Exceptions; + +public class LanguageTitleAlreadyExistsException : BaseException +{ + public LanguageTitleAlreadyExistsException() : base("title already exists") + { + } +} diff --git a/VizhTalk.Learning.Application/IUnitOfWork.cs b/VizhTalk.Learning.Application/IUnitOfWork.cs index a58b289..3fa9650 100644 --- a/VizhTalk.Learning.Application/IUnitOfWork.cs +++ b/VizhTalk.Learning.Application/IUnitOfWork.cs @@ -6,6 +6,8 @@ namespace VizhTalk.Learning.Application; public interface IUnitOfWork : IUnitOfWorkBase { ILanguageRepository Language { get; } + IFileGroupRepository FileGroup { get; } + IMediaRepository Media { get; } Task SaveChangeAsync(CancellationToken cancellationToken = default); } diff --git a/VizhTalk.Learning.Domain/Entities/Course.cs b/VizhTalk.Learning.Domain/Entities/Course.cs index a592d37..5611202 100644 --- a/VizhTalk.Learning.Domain/Entities/Course.cs +++ b/VizhTalk.Learning.Domain/Entities/Course.cs @@ -1,12 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using VizhTalk.BuildingBlocks.Domain; +using VizhTalk.Learning.Domain.TypeIds; +using VizhTalk.Learning.Domain.ValueObjects; -namespace VizhTalk.Learning.Domain.Entities +namespace VizhTalk.Learning.Domain.Entities; + +public class Course : Entity { - internal class Course + private Course() { + } + private Course(CourseTitle title, int order) + { + Title = title; + Order = order; + } + public CourseTitle Title { get; private set; } + public Media Image { get; private set; } + public int Order { get; private set; } + + internal static Course CreateNew(CourseTitle title, int order) + => new(title, order); + public void SetTitle(CourseTitle title) + => Title = title; + public void SetOrder(int order) + => Order = order; + public void SetImage(Media media) + => Image = media; } diff --git a/VizhTalk.Learning.Domain/Entities/Language.cs b/VizhTalk.Learning.Domain/Entities/Language.cs index ec64425..176c207 100644 --- a/VizhTalk.Learning.Domain/Entities/Language.cs +++ b/VizhTalk.Learning.Domain/Entities/Language.cs @@ -1,4 +1,5 @@ using VizhTalk.BuildingBlocks.Domain; +using VizhTalk.Learning.Domain.Exceptions; using VizhTalk.Learning.Domain.TypeIds; using VizhTalk.Learning.Domain.ValueObjects; @@ -6,24 +7,40 @@ namespace VizhTalk.Learning.Domain.Entities; public class Language : Entity, IAggregateRoot { - public Language() + private List _courses = new(); + private Language() { // only EF } - private Language(LanguageTitle title, Media icon) + private Language(LanguageTitle title) { Title = title; - Icon = icon; - } public LanguageTitle Title { get; private set; } public Media Icon { get; private set; } + public IReadOnlyList Courses => _courses; + public static Language CreateNew(LanguageTitle title) + => new(title); - public static Language CreateNew(LanguageTitle title,Media icon) - => new(title, icon); - public void SetTitle(LanguageTitle title) => Title = title; public void SetIcon(Media icon) - => Icon = icon; + { + if (Icon != null) + Icon.SetDeleted(true); + + Icon = icon; + } + public Course AddCourse(CourseTitle courseTitle) + { + var order = Courses.Count; + var course = Course.CreateNew(courseTitle, order); + _courses.Add(course); + return course; + } + public Course UpdateCourse(Course course) + { + var _course = _courses.FirstOrDefault(x => x.Id == course.Id) ?? throw new CourseNotFoundException(Title); + throw new Exception(); + } } diff --git a/VizhTalk.Learning.Domain/Entities/Media.cs b/VizhTalk.Learning.Domain/Entities/Media.cs index 9fb8277..e20ba2a 100644 --- a/VizhTalk.Learning.Domain/Entities/Media.cs +++ b/VizhTalk.Learning.Domain/Entities/Media.cs @@ -6,9 +6,28 @@ namespace VizhTalk.Learning.Domain.Entities; public class Media : Entity { + private Media() + { + + } + private Media(string name, + string contentType, + int size, + EntityType type) + { + Name = name; + ContentType = contentType; + Size = size; + EntityType = type; + } public string Name { get; private set; } public string ContentType { get; private set; } public int Size { get; private set; } public EntityType EntityType { get; private set; } + public static Media CreateNew(string name, + string contentType, + int size, + EntityType type) + => new(name, contentType, size, type); } diff --git a/VizhTalk.Learning.Domain/Exceptions/CourseNotFoundException.cs b/VizhTalk.Learning.Domain/Exceptions/CourseNotFoundException.cs new file mode 100644 index 0000000..7598386 --- /dev/null +++ b/VizhTalk.Learning.Domain/Exceptions/CourseNotFoundException.cs @@ -0,0 +1,8 @@ +using VizhTalk.BuildingBlocks.Domain.Contract; + +namespace VizhTalk.Learning.Domain.Exceptions; + +public class CourseNotFoundException(string languageTitle) + : Exception($"course not found in {languageTitle} language"),IGuardException +{ +} diff --git a/VizhTalk.Learning.Domain/Repositories/IFileGroupRepository.cs b/VizhTalk.Learning.Domain/Repositories/IFileGroupRepository.cs new file mode 100644 index 0000000..40813a1 --- /dev/null +++ b/VizhTalk.Learning.Domain/Repositories/IFileGroupRepository.cs @@ -0,0 +1,11 @@ +using VizhTalk.BuildingBlocks.Domain; +using VizhTalk.Learning.Domain.Entities; +using VizhTalk.Learning.Domain.TypeIds; +using VizhTalk.Learning.Domain.ValueObjects; + +namespace VizhTalk.Learning.Domain.Repositories; + +public interface IFileGroupRepository : IRepositoryBase +{ + Task GetByFileGroupKey(FileGroupKey key, CancellationToken cancellationToken = default); +} diff --git a/VizhTalk.Learning.Domain/Repositories/ILanguageRepository.cs b/VizhTalk.Learning.Domain/Repositories/ILanguageRepository.cs index 9f4feee..b8c8f68 100644 --- a/VizhTalk.Learning.Domain/Repositories/ILanguageRepository.cs +++ b/VizhTalk.Learning.Domain/Repositories/ILanguageRepository.cs @@ -2,10 +2,9 @@ using VizhTalk.Learning.Domain.Entities; using VizhTalk.Learning.Domain.TypeIds; -namespace VizhTalk.Learning.Domain.Repositories -{ - public interface ILanguageRepository : IRepositoryBase - { +namespace VizhTalk.Learning.Domain.Repositories; + +public interface ILanguageRepository : IRepositoryBase +{ - } } diff --git a/VizhTalk.Learning.Domain/Repositories/IMediaRepository.cs b/VizhTalk.Learning.Domain/Repositories/IMediaRepository.cs new file mode 100644 index 0000000..ebe4f34 --- /dev/null +++ b/VizhTalk.Learning.Domain/Repositories/IMediaRepository.cs @@ -0,0 +1,9 @@ +using VizhTalk.BuildingBlocks.Domain; +using VizhTalk.Learning.Domain.Entities; +using VizhTalk.Learning.Domain.TypeIds; + +namespace VizhTalk.Learning.Domain.Repositories; + +public interface IMediaRepository : IRepositoryBase +{ +} diff --git a/VizhTalk.Learning.Domain/TypeIds/CourseId.cs b/VizhTalk.Learning.Domain/TypeIds/CourseId.cs new file mode 100644 index 0000000..3fd152d --- /dev/null +++ b/VizhTalk.Learning.Domain/TypeIds/CourseId.cs @@ -0,0 +1,10 @@ +using VizhTalk.BuildingBlocks.Domain; + +namespace VizhTalk.Learning.Domain.TypeIds; + +public class CourseId : TypeIdValueBase +{ + public CourseId(long value) : base(value) + { + } +} diff --git a/VizhTalk.Learning.Domain/ValueObjects/CourseTitle.cs b/VizhTalk.Learning.Domain/ValueObjects/CourseTitle.cs new file mode 100644 index 0000000..2440ea2 --- /dev/null +++ b/VizhTalk.Learning.Domain/ValueObjects/CourseTitle.cs @@ -0,0 +1,18 @@ +using VizhTalk.BuildingBlocks.Domain.Guards; + +namespace VizhTalk.Learning.Domain.ValueObjects; + +public class CourseTitle +{ + public CourseTitle(string title) + { + title = title.Trim(); + Guard.NullOrEmpty(title, nameof(LanguageTitle)); + Guard.MaxLength(title, nameof(LanguageTitle), 60); + Value = title; + } + public string Value { get; private set; } + + public static implicit operator string(CourseTitle title) => title.Value; + +} diff --git a/VizhTalk.Learning.Domain/ValueObjects/FileGroupKey.cs b/VizhTalk.Learning.Domain/ValueObjects/FileGroupKey.cs index ad793ec..6971304 100644 --- a/VizhTalk.Learning.Domain/ValueObjects/FileGroupKey.cs +++ b/VizhTalk.Learning.Domain/ValueObjects/FileGroupKey.cs @@ -10,7 +10,7 @@ namespace VizhTalk.Learning.Domain.ValueObjects { public class FileGroupKey : ValueObject { - public static FileGroupKey LanguageIcon = new("Language"); + public static FileGroupKey IconLanguage = new("IconLanguage"); public FileGroupKey(string entityName, string? entityId = null) { Guard.NullOrEmpty(entityName, entityName); diff --git a/VizhTalk.Learning.Domain/VizhTalk.Learning.Domain.csproj b/VizhTalk.Learning.Domain/VizhTalk.Learning.Domain.csproj index c9f26b8..74589d7 100644 --- a/VizhTalk.Learning.Domain/VizhTalk.Learning.Domain.csproj +++ b/VizhTalk.Learning.Domain/VizhTalk.Learning.Domain.csproj @@ -8,7 +8,6 @@ - diff --git a/VizhTalk.Learning.Infrastructure/Repositories/FileGroupRepository.cs b/VizhTalk.Learning.Infrastructure/Repositories/FileGroupRepository.cs new file mode 100644 index 0000000..23c28db --- /dev/null +++ b/VizhTalk.Learning.Infrastructure/Repositories/FileGroupRepository.cs @@ -0,0 +1,24 @@ + + +using Microsoft.EntityFrameworkCore; +using VizhTalk.BuildingBlocks.Infrastructure; +using VizhTalk.Learning.Domain.Entities; +using VizhTalk.Learning.Domain.Repositories; +using VizhTalk.Learning.Domain.TypeIds; +using VizhTalk.Learning.Domain.ValueObjects; +using VizhTalk.Learning.Infrastructure.EF; + +namespace VizhTalk.Learning.Infrastructure.Repositories; + +public class FileGroupRepository : RepositoryBase, IFileGroupRepository +{ + public FileGroupRepository(LearningDbContext dbContext) : base(dbContext) + { + } + + public async Task GetByFileGroupKey(FileGroupKey key, CancellationToken cancellationToken = default) + { + var fileGroup = await Entity.FirstOrDefaultAsync(x=>x.Key == key, cancellationToken); + return fileGroup; + } +} diff --git a/VizhTalk.Learning.Infrastructure/Repositories/MediaRepository.cs b/VizhTalk.Learning.Infrastructure/Repositories/MediaRepository.cs new file mode 100644 index 0000000..22a5760 --- /dev/null +++ b/VizhTalk.Learning.Infrastructure/Repositories/MediaRepository.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using VizhTalk.BuildingBlocks.Infrastructure; +using VizhTalk.Learning.Domain.Entities; +using VizhTalk.Learning.Domain.Repositories; +using VizhTalk.Learning.Domain.TypeIds; + +namespace VizhTalk.Learning.Infrastructure.Repositories; + +public class MediaRepository : RepositoryBase, IMediaRepository +{ + public MediaRepository(DbContext dbContext) : base(dbContext) + { + } +} diff --git a/VizhTalk.Learning.Infrastructure/UnitOfWork.cs b/VizhTalk.Learning.Infrastructure/UnitOfWork.cs index 252e320..3e367e5 100644 --- a/VizhTalk.Learning.Infrastructure/UnitOfWork.cs +++ b/VizhTalk.Learning.Infrastructure/UnitOfWork.cs @@ -1,4 +1,5 @@ -using VizhTalk.BuildingBlocks.Infrastructure; +using System.Runtime.CompilerServices; +using VizhTalk.BuildingBlocks.Infrastructure; using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching; using VizhTalk.Learning.Application; using VizhTalk.Learning.Domain.Repositories; @@ -22,6 +23,30 @@ namespace VizhTalk.Learning.Infrastructure } } + private IFileGroupRepository _fileGroup; + public IFileGroupRepository FileGroup + { + get + { + if (_fileGroup == null) + _fileGroup = new FileGroupRepository(learningDbContext); + + return _fileGroup; + } + } + + private IMediaRepository _media; + public IMediaRepository Media + { + get + { + if(_media == null) + _media = new MediaRepository(learningDbContext); + + return _media; + } + } + public async Task SaveChangeAsync(CancellationToken cancellationToken = default) => await learningDbContext.SaveChangesAsync(cancellationToken); } diff --git a/VizhTalk.Services.Grpc.Learning/Proto/Languages/Commands.proto b/VizhTalk.Services.Grpc.Learning/Proto/Languages/Commands.proto index 8ed7bde..46575e8 100644 --- a/VizhTalk.Services.Grpc.Learning/Proto/Languages/Commands.proto +++ b/VizhTalk.Services.Grpc.Learning/Proto/Languages/Commands.proto @@ -14,5 +14,5 @@ message CreateLanguageCommand{ message UpdateLanguageCommand{ Id Id = 1; string title = 2; - bytes icon = 3; + optional bytes icon = 3; } \ No newline at end of file