rbeauchamp / Swashbuckle.OData

Extends Swashbuckle with OData v4 support!
Other
129 stars 96 forks source link

500 : {"Message":"An error has occurred.","ExceptionMessage":"Object reference not set to an instance of an object.","ExceptionType":"System.NullReferenceException","StackTrace":" at #158

Closed rohitiscancerian closed 4 years ago

rohitiscancerian commented 7 years ago

500 : {"Message":"An error has occurred.","ExceptionMessage":"Object reference not set to an instance of an object.","ExceptionType":"System.NullReferenceException","StackTrace":" at Swashbuckle.OData.ODataSwaggerProvider.CreatePathItem(IEnumerable1 apiDescriptions, SchemaRegistry schemaRegistry) in C:\\Users\\rbeauchamp\\Documents\\GitHub\\Swashbuckle.OData\\Swashbuckle.OData\\ODataSwaggerProvider.cs:line 155\r\n at Swashbuckle.OData.ODataSwaggerProvider.<>c__DisplayClass5_0.<GenerateSwagger>b__3(IGrouping2 group) in C:\Users\rbeauchamp\Documents\GitHub\Swashbuckle.OData\Swashbuckle.OData\ODataSwaggerProvider.cs:line 83\r\n at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func2 elementSelector, IEqualityComparer1 comparer)\r\n at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func2 elementSelector)\r\n at Swashbuckle.OData.ODataSwaggerProvider.GenerateSwagger(String rootUrl, String apiVersion) in C:\\Users\\rbeauchamp\\Documents\\GitHub\\Swashbuckle.OData\\Swashbuckle.OData\\ODataSwaggerProvider.cs:line 79\r\n at Swashbuckle.OData.ODataSwaggerProvider.<>c__DisplayClass4_0.<GetSwagger>b__1() in C:\\Users\\rbeauchamp\\Documents\\GitHub\\Swashbuckle.OData\\Swashbuckle.OData\\ODataSwaggerProvider.cs:line 50\r\n at System.Lazy1.CreateValue()\r\n at System.Lazy1.LazyInitValue()\r\n at System.Lazy1.get_Value()\r\n at Swashbuckle.OData.ODataSwaggerProvider.GetSwagger(String rootUrl, String apiVersion) in C:\Users\rbeauchamp\Documents\GitHub\Swashbuckle.OData\Swashbuckle.OData\ODataSwaggerProvider.cs:line 53\r\n at Swashbuckle.Application.SwaggerDocsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpMessageInvoker.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpRoutingDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.Tracing.Tracers.RequestMessageHandlerTracer.<>cDisplayClass4.b1()\r\n at System.Web.Http.Tracing.ITraceWriterExtensions.TraceBeginEndAsync[TResult](ITraceWriter traceWriter, HttpRequestMessage request, String category, TraceLevel level, String operatorName, String operationName, Action1 beginTrace, Func1 execute, Action2 endTrace, Action1 errorTrace)\r\n at System.Web.Http.Tracing.Tracers.RequestMessageHandlerTracer.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.HttpServer.d__0.MoveNext()"} http://localhost:61797/swagger/docs/v1

rohitiscancerian commented 7 years ago

Following is my config . Swashbuckle.Odata v 3.4.0

c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c, GlobalConfiguration.Configuration).Configure(odataConfig => { odataConfig.IncludeNavigationProperties(); odataConfig.EnableSwaggerRequestCaching(); }));

eloekset commented 6 years ago

I get the same exception once I add a base controller an inherit from that instead of directly from ODataController.

public abstract class DefaultAccessODataBaseController<TEntity, TKey> : ODataController where TEntity : class
    {
        protected SwaggerTestDbContext _dbContext;
        protected abstract Expression<Func<TEntity, TKey>> KeyProperty { get; }

        public DefaultAccessODataBaseController(SwaggerTestDbContext dbContext)
            : base()
        {
            _dbContext = dbContext;
        }

        [EnableQuery(PageSize = 20, HandleNullPropagation = System.Web.OData.Query.HandleNullPropagationOption.False, AllowedQueryOptions = System.Web.OData.Query.AllowedQueryOptions.All)]
        // HandleNullPropagationOption.False is a workaround for issue, which should have been fixed in EFCore 2.0.0-preview1:
        // https://github.com/aspnet/EntityFramework/issues/9169
        [ODataRoute]
        public IQueryable<TEntity> Get()
        {
            return _dbContext.Set<TEntity>().AsNoTracking().AsQueryable();
        }

        [EnableQuery(PageSize = 20, HandleNullPropagation = System.Web.OData.Query.HandleNullPropagationOption.False, AllowedQueryOptions = System.Web.OData.Query.AllowedQueryOptions.All)]
        // HandleNullPropagationOption.False is a workaround for issue, which should have been fixed in EFCore 2.0.0-preview1:
        // https://github.com/aspnet/EntityFramework/issues/9169
        public SingleResult<TEntity> Get([FromODataUri] TKey key)
        {
            var filterByKey = GetFilterByKeyExpression(key);
            var entity = _dbContext.Set<TEntity>().AsNoTracking().Where(filterByKey);

            return SingleResult.Create(entity);
        }

        // PUT: odata/Entity(D35E6C2C-1548-45E4-B686-27AEFB387BF1)
        public virtual async Task<IHttpActionResult> Put([FromODataUri] TKey key, Delta<TEntity> patch)
        {
            var entity = await _dbContext.Set<TEntity>().AsTracking().FirstOrDefaultAsync(GetFilterByKeyExpression(key));

            if (entity == null)
            {
                return NotFound();
            }

            patch.Put(entity);

            await ApplyRulesBeforeValidatingAndSavingAsync(entity);
            Validate(entity);

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            try
            {
                SetLastEditedAtAndLastEditedBy(entity);
                _dbContext.Update(entity);
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await EntityExistsAsync(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            await OnAfterSave(entity);

            return Updated(entity);
        }

        // POST: odata/Entity
        public virtual async Task<IHttpActionResult> Post(TEntity entity)
        {
            await ApplyRulesBeforeValidatingAndSavingAsync(entity);

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            try
            {
                SetLastEditedAtAndLastEditedBy(entity);
                _dbContext.Add(entity);
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateException)
            {
                var key = GetKeyValue(entity, KeyProperty);

                if (await EntityExistsAsync(key))
                {
                    return Conflict();
                }
                else
                {
                    throw;
                }
            }

            await OnAfterSave(entity);

            return Created(entity);
        }

        // PATCH: odata/Entity(D35E6C2C-1548-45E4-B686-27AEFB387BF1)
        [AcceptVerbs("PATCH", "MERGE")]
        public virtual async Task<IHttpActionResult> Patch([FromODataUri] TKey key, Delta<TEntity> patch)
        {
            var entity = await _dbContext.Set<TEntity>().AsTracking().FirstOrDefaultAsync(GetFilterByKeyExpression(key));

            if (entity == null)
            {
                return NotFound();
            }

            patch.Patch(entity);
            await ApplyRulesBeforeValidatingAndSavingAsync(entity);
            Validate(entity);

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            try
            {
                SetLastEditedAtAndLastEditedBy(entity);
                _dbContext.Update(entity);
                await _dbContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!await EntityExistsAsync(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            await OnAfterSave(entity);

            return Updated(entity);
        }

        // DELETE: odata/Entity(D35E6C2C-1548-45E4-B686-27AEFB387BF1)
        public virtual async Task<IHttpActionResult> Delete([FromODataUri] TKey key)
        {
            var entity = await _dbContext.Set<TEntity>().FindAsync(key);

            try
            {
                await ValidateDeleteEntityAsync(entity);
            }
            catch (Exception ex)
            {
                throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new ODataError()
                {
                    ErrorCode = "13456",
                    Message = ex.Message
                }));
            }

            if (entity == null)
            {
                return NotFound();
            }

            _dbContext.Remove(entity);
            await _dbContext.SaveChangesAsync();
            await OnAfterDelete(entity);

            return StatusCode(HttpStatusCode.NoContent);
        }

        protected async Task<bool> EntityExistsAsync(TKey key)
        {
            return await _dbContext.Set<TEntity>().AnyAsync(GetFilterByKeyExpression(key));
        }

        protected TKey GetKeyValue(TEntity entity, Expression<Func<TEntity, TKey>> propertyLambda)
        {
            PropertyInfo propInfo = GetKeyPropertyInfo(propertyLambda);
            TKey keyValue = (TKey)propInfo.GetValue(entity);

            return keyValue;
        }

        protected PropertyInfo GetKeyPropertyInfo(Expression<Func<TEntity, TKey>> propertyLambda)
        {
            Type type = typeof(TEntity);
            MemberExpression member = propertyLambda.Body as MemberExpression;

            if (member == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda.ToString()));

            PropertyInfo propInfo = member.Member as PropertyInfo;

            if (propInfo == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", propertyLambda.ToString()));

            if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType))
                throw new ArgumentException(string.Format("Expresion '{0}' refers to a property that is not from type {1}.", propertyLambda.ToString(), type));

            return propInfo;
        }

        protected Expression<Func<TEntity, bool>> GetFilterByKeyExpression(TKey keyValue)
        {
            var propInfo = GetKeyPropertyInfo(KeyProperty);

            var parameter = Expression.Parameter(typeof(TEntity), "x");
            var lambda = Expression.Lambda<Func<TEntity, bool>>(
                    Expression.Equal(
                        Expression.PropertyOrField(parameter, propInfo.Name),
                        Expression.Constant(keyValue)
                    ),
                    parameter
                );

            return lambda;
        }

        private void SetLastEditedAtAndLastEditedBy(TEntity entity)
        {
            var lastEditedAtPropertyInfo = entity.GetType().GetProperty(nameof(Customer.LastEditedAt));
            lastEditedAtPropertyInfo.SetValue(entity, DateTimeOffset.UtcNow);
            var lastEditedByPropertyInfo = entity.GetType().GetProperty(nameof(Customer.LastEditedBy));
            lastEditedByPropertyInfo.SetValue(entity, RequestContext.Principal.Identity.Name);
        }

        protected virtual async Task ApplyRulesBeforeValidatingAndSavingAsync(TEntity entity)
        {
            // Can be overridden in subclass.
            await Task.CompletedTask;
        }

        /// <summary>
        /// Override to throw a descriptive exception if the given <paramref name="entity"/> cannot be deleted.
        /// </summary>
        /// <param name="entity">Entity to be deleted</param>
        protected virtual async Task ValidateDeleteEntityAsync(TEntity entity)
        {
            await Task.CompletedTask;
        }

        protected virtual async Task OnAfterSave(TEntity entity)
        {
            await Task.CompletedTask;
        }

        protected virtual async Task OnAfterDelete(TEntity entity)
        {
            // Can be overridden in subclass.
            await Task.CompletedTask;
        }
    }

And then a controller to represent an entity:

public class CustomersController : DefaultAccessODataBaseController<Customer, Guid>
    {
        protected override Expression<Func<Customer, Guid>> KeyProperty => c => c.CustomerId;

        public CustomersController(SwaggerTestDbContext dbContext)
            : base(dbContext)
        {
        }

        [EnableQuery]
        [ODataRoute]
        public IQueryable<CustomerLicense> GetCustomerLicenses([FromODataUri] Guid key)
        {
            var customerLicenses = _dbContext.CustomerLicenses.Where(cl => cl.CustomerId == key).AsNoTracking();

            return customerLicenses;
        }

        protected override async Task ApplyRulesBeforeValidatingAndSavingAsync(Customer entity)
        {
            await base.ApplyRulesBeforeValidatingAndSavingAsync(entity);

            if (entity.CustomerNo <= 0)
            {
                entity.CustomerNo = _dbContext.Customers.Max(c => c.CustomerNo) + 1;
            }
        }

        protected override async Task ValidateDeleteEntityAsync(Customer entity)
        {
            await ValidateNoLicenseReferencesAsync(entity);
        }

        private async Task ValidateNoLicenseReferencesAsync(Customer entity)
        {
            int licenses = 0;

            if (entity == null)
            {
                // Default handling in base controller delete method. Don't throw exception for this.
            }

            if (entity.CustomerLicenses == null)
            {
                licenses = _dbContext.CustomerLicenses.Where(cl => cl.CustomerId == entity.CustomerId).Count();
            }
            else
            {
                licenses = entity.CustomerLicenses.Count;
            }

            if (licenses > 0)
            {
                throw new InvalidOperationException($"Customer is referenced by {licenses} licenses and cannot be deleted.");
            }

            await Task.CompletedTask;
        }
    }
eloekset commented 6 years ago

Regarding my previous comment, I pulled down the sources and debugged the ODataSwaggerProvider.CreatePathItem() method. It throws NullReferenceException because I don't have a ConflictingActionsResolver configured.

Once I uncomment the default ResolveConflictingActions line in SwaggerConfig.cs it runs successfully:

c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());

It would be better if an exception with a more descriptive message was thrown.

eloekset commented 6 years ago

Using the assembly from my proposed PRs #170 and #171, I get this Exception message instead of the one in the initial comment of this issue: 500 : {"$id":"1","Message":"An error has occurred.","ExceptionMessage":"ResolveConflictingActions is not configured for Swagger.","ExceptionType":"System.InvalidOperationException","StackTrace":" at Swashbuckle.OData.ODataSwaggerProvider.CreatePathItem(IEnumerable1 apiDescriptions, SchemaRegistry schemaRegistry) in C:\\GitHub\\eloekset\\Swashbuckle.OData\\Swashbuckle.OData\\ODataSwaggerProvider.cs:line 155\r\n at Swashbuckle.OData.ODataSwaggerProvider.<>c__DisplayClass5_0.<GenerateSwagger>b__3(IGrouping2 group) in C:\GitHub\eloekset\Swashbuckle.OData\Swashbuckle.OData\ODataSwaggerProvider.cs:line 83\r\n at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func2 elementSelector, IEqualityComparer1 comparer)\r\n at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable1 source, Func2 keySelector, Func`2 elementSelector)\r\n at Swashbuckle.OData.ODataSwaggerProvider.GenerateSwagger(String rootUrl, String apiVersion) in C:\GitHub\eloekset\Swashbuckle.OData\Swashbuckle.OData\ODataSwaggerProvider.cs:line 79\r\n at Swashbuckle.OData.ODataSwaggerProvider.GetSwagger(String rootUrl, String apiVersion) in C:\GitHub\eloekset\Swashbuckle.OData\Swashbuckle.OData\ODataSwaggerProvider.cs:line 56\r\n at Swashbuckle.Application.SwaggerDocsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpMessageInvoker.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpRoutingDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.DelegatingHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.HttpServer.d__0.MoveNext()"} http://localhost:64848/swagger/docs/v1