Question Upgrading from ASP.NET Web API 2 to ASP.NET Web API Core 6

raysefo

Well-known member
Joined
Feb 22, 2019
Messages
361
Programming Experience
10+
Hello,

I have had this ASP.NET Web API application running smoothly for 2-3 years in a live environment. I used EF, generic repository, unit of work and unity dependency injection in this application. I want to upgrade my application to take advantage of Core 6.

How can I make this transition seamlessly? Can I have your recommendations?

Thank you.
 
Have you looked at the Microsoft upgrade guides?
 
I don't that they can do that because not everyone is using the same framework for doing IoC, repository, etc.

You'll likely need to look at your individual frameworks to see how they fit into the new .NET (Core) ecosystem. For example, if you use Autofac as your IoC, they have a guide on how to integrate with .NET (Core)'s IoC system. I suspect that Unity would have the same.
 
My recommendation is to upgrade piece meal instead of trying to do a big bang.
 
That's what I am trying to do :) The first hurdle is in the legacy application I was using HttpWebResponse while calling 3rd party web services. But it is obsolete. How can I convert this into IHttpClientFactory?

3rd party web service call:
public static class EzPinApiCaller
    {
        
        public static async Task<HttpResponseMessage?> CallToken(string url)
        {
            
            //HTTPWebRequest DEV
            var request = (HttpWebRequest)WebRequest.Create(WebConfigurationManager.AppSettings["EzPinService"] + url);

            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "POST";
            request.KeepAlive = false;

            //Create a list of your parameters
            var postParams = new List<KeyValuePair<string, string>>(){
                new KeyValuePair<string, string>("client_id", WebConfigurationManager.AppSettings["ClientId"]) ,
                new KeyValuePair<string, string>("secret_key", WebConfigurationManager.AppSettings["Secret"])
            };
            var keyValueContent = postParams;
            var formUrlEncodedContent = new FormUrlEncodedContent(keyValueContent);
            var urlEncodedString = await formUrlEncodedContent.ReadAsStringAsync();

            using (var streamWriter = new StreamWriter(await request.GetRequestStreamAsync()))
            {
                await streamWriter.WriteAsync(urlEncodedString);
            }

            var httpResponse = (HttpWebResponse)(await request.GetResponseAsync());

            var response = new HttpResponseMessage
            {
                StatusCode = httpResponse.StatusCode,
                Content = new StreamContent(httpResponse.GetResponseStream()),
            };

            return response;
        }

        public static async Task<HttpResponseMessage> CallOrder(string url, string token, WatsonOrderRequest watsonOrderRequest)
        {
            HttpResponseMessage response = null;
            try
            {
                //HTTPWebRequest DEV
                var request = (HttpWebRequest)WebRequest.Create(WebConfigurationManager.AppSettings["EzPinService"] + url);

                request.ContentType = "application/x-www-form-urlencoded";
                request.Method = "POST";
                request.KeepAlive = false;
                request.Headers.Add("Authorization", "Bearer " + token);

                //Create a list of your parameters
                var postParams = new List<KeyValuePair<string, string>>(){
                    new KeyValuePair<string, string>("sku", watsonOrderRequest.sku.ToString()) ,
                    new KeyValuePair<string, string>("quantity", watsonOrderRequest.quantity.ToString()),
                    new KeyValuePair<string, string>("pre_order", watsonOrderRequest.pre_order.ToString()),
                    new KeyValuePair<string, string>("price", watsonOrderRequest.price),
                    new KeyValuePair<string, string>("reference_code", watsonOrderRequest.reference_code.ToString())
                };

                var keyValueContent = postParams;
                var formUrlEncodedContent = new FormUrlEncodedContent(keyValueContent);
                var urlEncodedString = await formUrlEncodedContent.ReadAsStringAsync();

                using (var streamWriter = new StreamWriter(await request.GetRequestStreamAsync()))
                {
                    await streamWriter.WriteAsync(urlEncodedString);
                }

                var httpResponse = (HttpWebResponse)(await request.GetResponseAsync());
                response = new HttpResponseMessage
                {
                    StatusCode = httpResponse.StatusCode,
                    Content = new StreamContent(httpResponse.GetResponseStream()),
                };
            }
            catch (WebException ex)
            {
                //Identify EZPIN error details
                var resp = new StreamReader(ex.Response.GetResponseStream()).ReadToEnd();
                dynamic obj = JsonConvert.DeserializeObject(resp);
                var detail = obj.detail;
                var code = obj.code;
                response = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(detail.ToString(), System.Text.Encoding.UTF8, "application/json") };
            }


            return response;
        }

        public static async Task<HttpResponseMessage> CallCards(string url, string token)
        {
            HttpResponseMessage response = null;

            //HTTPWebRequest DEV
            var request = (HttpWebRequest)WebRequest.Create(WebConfigurationManager.AppSettings["EzPinService"] + url);

            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "GET";
            request.KeepAlive = false;
            request.Headers.Add("Authorization", "Bearer " + token);


            var httpResponse = (HttpWebResponse)(await request.GetResponseAsync());
            response = new HttpResponseMessage
            {
                StatusCode = httpResponse.StatusCode,
                Content = new StreamContent(httpResponse.GetResponseStream()),
            };

            return response;
        }
    }
 
Last edited:
HttpWebRequest is not obsolete. I think that you are mistaking the recommendation in the documentation to not use this class for new development as a warning that the class is obsolete. Microsoft has a completely different text for obsolete classes. Also note that the recommended HttpClient just uses HttpWebRequest under the covers. Since you are porting code, it is not new development. You can continue to use the HttpWebRequest. Just add it to your backlog as technical debt to migrate to HttpClient later. It shouldn't impede your current migration efforts to move to now.
 
Last edited:
If your third party EzPinApi supports openApi specs you can have a tool like NSwag, AutoRest etc generate all that calling code for it using the modern approach
 
Asp.net core web API supports DI as default right? So I don't need any third party framework like Unity. If so how can I manage this dependency below in the program.cs?

container.RegisterType<IGameServices, GameServices>().RegisterType<UnitOfWork>(new TransientLifetimeManager());
 
Did you look at the DI documentation that Microsoft provides?
 
Yes.
 
I tried like this;

C#:
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<GameApiDbContext>(
    options => options.UseSqlServer("name=ConnectionStrings:GameApiDbContext"));
builder.Services.AddTransient<IOyunPalasServices, OyunPalasServices>().AddTransient<UnitOfWork>();

builder.Services.AddTransient<IUserValidate, UserValidate>();

builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

But got this error message:
C#:
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: OyunPalasGame.Services.IOyunPalasServices Lifetime: Transient ImplementationType: OyunPalasGame.Services.OyunPalasServices': Unable to resolve service for type 'OyunPalasGame.Data.GenericGameRepository`1[OyunPalasGame.Entity.GameRequest]' while attempting to activate 'OyunPalasGame.Data.UnitOfWork'.) (Error while validating the service descriptor 'ServiceType: OyunPalasGame.Data.UnitOfWork Lifetime: Transient ImplementationType: OyunPalasGame.Data.UnitOfWork': Unable to resolve service for type 'OyunPalasGame.Data.GenericGameRepository`1[OyunPalasGame.Entity.GameRequest]' while attempting to activate 'OyunPalasGame.Data.UnitOfWork'.) (Error while validating the service descriptor 'ServiceType: OyunPalasGame.Services.IUserValidate Lifetime: Transient ImplementationType: OyunPalasGame.Services.UserValidate': Unable to resolve service for type 'OyunPalasGame.Data.GenericGameRepository`1[OyunPalasGame.Entity.GameRequest]' while attempting to activate 'OyunPalasGame.Data.UnitOfWork'.)
 
If you look at the error, it looks like you did not register all the dependencies. I can only scroll a little on my phone but the first missing dependency reported was a game repository that takes a generic type.
 
Last edited:
There was a problem with the entity namespace, which I fixed. Now I am getting this response when trying with Postman.

C#:
System.InvalidOperationException: The entity type 'List<string>' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateNonNullPrimaryKeys(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.Set[TEntity]()
   at OyunPalasGame.Data.GenericGameRepository`1..ctor(GameApiDbContext context) in C:\Users\197199\Desktop\Development\Backup\OyunPalasGame\OyunPalasGame.Data\GenericGameRepository.cs:line 15
   at OyunPalasGame.Data.UnitOfWork.get_ProductCodeRepository() in C:\Users\197199\Desktop\Development\Backup\OyunPalasGame\OyunPalasGame.Data\UnitOfWork.cs:line 52
   at OyunPalasGame.Services.OyunPalasServices.CallGameStock(RequestDto requestDto) in C:\Users\197199\Desktop\Development\Backup\OyunPalasGame\OyunPalasGame.Services\OyunPalasServices.cs:line 121
   at OyunPalasGame.Services.OyunPalasServices.RazerPurchase(RequestDto requestDto) in C:\Users\197199\Desktop\Development\Backup\OyunPalasGame\OyunPalasGame.Services\OyunPalasServices.cs:line 391
   at OyunPalasAPI.Controllers.OyunPalasController.PurchaseGame(RequestDto game) in C:\Users\197199\Desktop\Development\Backup\OyunPalasGame\OyunPalasGameAPI\Controllers\OyunPalasGameController.cs:line 32
   at lambda_method5(Closure, Object)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:5236
User-Agent: PostmanRuntime/7.30.0
Accept-Encoding: gzip, deflate, br
Authorization: Bearer N7N59vP_
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
Postman-Token: 4df4c976-ea93-4fce-a12b-66c19b1e68cf

Here is the line where the error pops up:
C#:
var productCode = _unitOfWork.ProductCodeRepository.GetByCode(p => p.ClientCode == requestDto.ProductCode);

Here is the unit of work:
C#:
using OyunPalasGame.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace OyunPalasGame.Data
{
    public class UnitOfWork : IDisposable
    {
        private readonly GameApiDbContext _context;
        private readonly GenericGameRepository<GameRequest> _gameRequestRepository;
        private readonly GenericGameRepository<GameResponse> _gameResponseRepository;
        private readonly GenericGameRepository<GameConfirmRequest> _gameConfirmRequestRepository;
        private readonly GenericGameRepository<GameConfirmResponse> _gameConfirmResponseRepository;
        private readonly GenericGameRepository<GameBank> _gameBankRepository;
        private readonly GenericGameRepository<GameBankPin> _gameBankPinRepository;
        private readonly GenericGameRepository<ConfirmCancel> _confirmCancelRepository;
        private readonly GenericGameRepository<Recon> _reconRepository;
        private readonly GenericGameRepository<Company> _userRepository;
        private readonly GenericGameRepository<ProductCode> _productCodeRepository;
        private readonly GenericGameRepository<Reconciliation> _reconciliationRepository;
        private readonly GenericGameRepository<ReconciliationDetail> _reconciliationDetailRepository;
        private readonly GenericGameRepository<ReconciliationResponse> _reconciliationResponseRepository;

        public UnitOfWork(GameApiDbContext context)
        {
            _context = context;
          
        }

        public GenericGameRepository<GameRequest> GameRepository => _gameRequestRepository ?? new GenericGameRepository<GameRequest>(_context);

        public GenericGameRepository<GameResponse> GameResponseRepository => _gameResponseRepository ?? new GenericGameRepository<GameResponse>(_context);


        public GenericGameRepository<GameConfirmRequest> GameConfirmRequestRepository => _gameConfirmRequestRepository ?? new GenericGameRepository<GameConfirmRequest>(_context);

        public GenericGameRepository<GameConfirmResponse> GameConfirmResponseRepository => _gameConfirmResponseRepository ?? new GenericGameRepository<GameConfirmResponse>(_context);

        public GenericGameRepository<GameBank> GameBankRepository => _gameBankRepository ?? new GenericGameRepository<GameBank>(_context);

        public GenericGameRepository<GameBankPin> GameBankPinRepository => _gameBankPinRepository ?? new GenericGameRepository<GameBankPin>(_context);

        public GenericGameRepository<ConfirmCancel> ConfirmCancelRepository => _confirmCancelRepository ?? new GenericGameRepository<ConfirmCancel>(_context);

        public GenericGameRepository<Recon> ReconRepository => _reconRepository ?? new GenericGameRepository<Recon>(_context);

        public GenericGameRepository<Company> UserRepository => _userRepository ?? new GenericGameRepository<Company>(_context);

        public GenericGameRepository<ProductCode> ProductCodeRepository => _productCodeRepository ?? new GenericGameRepository<ProductCode>(_context);

        public GenericGameRepository<Reconciliation> ReconciliationRepository => _reconciliationRepository ?? new GenericGameRepository<Reconciliation>(_context);

        public GenericGameRepository<ReconciliationDetail> ReconciliationDetailRepository => _reconciliationDetailRepository ?? new GenericGameRepository<ReconciliationDetail>(_context);

        public GenericGameRepository<ReconciliationResponse> ReconciliationResponseRepository => _reconciliationResponseRepository ?? new GenericGameRepository<ReconciliationResponse>(_context);

        public void Save()
        {
            _context.SaveChanges();
        }


        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }

        private bool _disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    _context.Dispose();
                }
            }
            _disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

I have no clue what The entity type 'List<string>' requires a primary key to be defined ??!
 
Back
Top Bottom