ASPNET Core replacement for [Session] = - c#

We are migrating from ASPNET MVC5 to ASPNET Core meaning we need to refactor some code.
We were using Session = model to store the model in the session, then retrieving it from another Controller.
We understand this option has been discontinued in Core.
We have attempted to use:
HttpContext.Session.SetString("Data", JsonConvert.SerializeObject(model));
However, when Deserialising by using:
var json = HttpContext.Session.GetString("Data");
var model = JsonConvert.DeserializeObject<SearchListViewModel>(json);
The resulting model does not come back the same - it is one long string rather than a structured list (which is was before Serialising).
Is there a better way to achieve passing a model from one controller to another?

To configure Session in your project, you need to do the following:
In your Startup.cs, under the Configure method, add this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSession();
}
And then under the ConfigureServices method, add this:
public void ConfigureServices(IServiceCollection services)
{
//Added for session state
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(10);
});
}
Create a model class of your object type (whatever you want to store). I am giving a simple example:
public class SearchListViewModel
{
public int SearchID{ get; set; }
public string SearchName{ get; set; }
//so on
}
Then create a SessionExtension helper to set and retrieve your complex object as JSON:
public static class SessionExtensions
{
public static void SetObjectAsJson(this ISession session, string key, object value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
public static T GetObjectFromJson<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}
Then finally set the complex object in your session as:
var search= new SearchListViewModel();
search.SearchID= 1;
search.SearchName= "Test";
HttpContext.Session.SetObjectAsJson("SearchListViewModel", search);
To retrieve your complex object in your session:
var searhDetails = HttpContext.Session.GetObjectFromJson<SearchListViewModel>("SearchListViewModel");
int searchID= SearchListViewModel.SearchID;
string searchName= SearchListViewModel.SearchName;

you can still use session if you need. You just need to config it in startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSession();
....another your code
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSession();
... another your code
}

Related

C# Polymorphic models with NSwag

Within my Asp.Net Core v5 application we have the following models
public class StorageRecordTypeMetadataBase
{
public string PropertyName { get; set; }
public bool Required { get; set; }
}
public class StringRecordTypeMetadata: StorageRecordTypeMetadataBase
{
public string? ValidationRegex { get; set; }
}
public class NumericRecordTypeMetadata : StorageRecordTypeMetadataBase
{
public int MinValue { get; set; }
public int MaxValue { get; set; }
}
In my application Startup.cs I have registered Swagger and NSwag setup as following:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerDocument();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseOpenApi();
app.UseSwaggerUi3();
}
}
In order to support polymorphism, I have followed the guideline as written by NSwag creator: Guide on inheritance in NSwag, and here is how my model looks like updated:
[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[KnownType(typeof(StringRecordTypeMetadata))]
[KnownType(typeof(NumericRecordTypeMetadata))]
public class StorageRecordTypeMetadataBase
{
public string PropertyName { get; set; }
public bool Required { get; set; }
}
As soon as I run the application, swagger fails as swagger.json could not be generated. Upon investigating the issue I can see the following error message once I try to manually navigate to /swagger/v1/swagger.json
System.MissingMethodException: Method not found: 'System.String
Namotion.Reflection.XmlDocsExtensions.GetXmlDocsSummary(System.Reflection.MemberInfo)'.
at
NSwag.Generation.Processors.OperationSummaryAndDescriptionProcessor.ProcessSummary(OperationProcessorContext
context, List1 attributes) at NSwag.Generation.Processors.OperationSummaryAndDescriptionProcessor.Process(OperationProcessorContext context) at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.RunOperationProcessors(OpenApiDocument document, ApiDescription apiDescription, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List1 allOperations, OpenApiDocumentGenerator
swaggerGenerator, OpenApiSchemaResolver schemaResolver) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument
document, Type controllerType, List1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver) at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateForControllers(OpenApiDocument document, IGrouping2[] apiGroups, OpenApiSchemaResolver
schemaResolver) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(ApiDescriptionGroupCollection
apiDescriptionGroups) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(Object
serviceProvider) at
NSwag.AspNetCore.OpenApiDocumentProvider.GenerateAsync(String
documentName) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GenerateDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.Invoke(HttpContext
context) at
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext
context)
I have tried to include the referenced package Namotion.Reflection even myself but that did not help either. Is there anything that I have missed during my configuration?
This was supposed to add discriminator field within the base model so that it would be automatically recognized when I generate my models on front end (React) side. I can achieve this behavior by moving away from NSwag, to Swashbuckle like following:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerGen(c =>
{
c.UseAllOfForInheritance();
c.SelectSubTypesUsing(baseType =>
{
return typeof(StorageRecordType).Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType));
});
c.SelectDiscriminatorNameUsing((baseType) => "itemType");
c.SelectDiscriminatorValueUsing((subType) => subType.Name);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger((SwaggerOptions c) => { });
app.UseSwaggerUI();
}
}
However this completely breaks the NSwag generation process on my React side. Methods from all controllers are put together into a single Client (instead of being separated per controller name), plus some of the classes required in the parameters seem to be gone as well.
How can I fix the NSwag in order to get the discriminator value in my swagger.json response?
Ok so the issue was, that I was using older version of Nswag.AspNetCore. Instead of version 13.10.8 I upgraded to 13.15.5, which works great with package NJsonSchema v 10.6.6

Mapping dynamic odata routes with ASP.NET Core OData 8.0

I've got an application where the EDM datatypes are generated during the runtime of the application (they can even change during runtime). Based loosely on OData DynamicEDMModelCreation Sample - refactored to use the new endpoint routing. There the EDM model is dynamically generated at runtime and all requests are forwarded to the same controller.
Now I wanted to update to the newest ASP.NET Core OData 8.0 and the whole routing changed so that the current workaround does not work anymore.
I've read the two blog posts of the update Blog1Blog2 and it seems that I can't use the "old" workaround anymore as the function MapODataRoute() within the endpoints is now gone. It also seems that none of the built-in routing convention work for my use-case as all require the EDM model to be present at debug time.
Maybe I can use a custom IODataControllerActionConvention. I tried to active the convention by adding it to the Routing Convention but it seems I'm still missing a piece how to activate it.
services.TryAddEnumerable(
ServiceDescriptor.Transient<IODataControllerActionConvention, MyEntitySetRoutingConvention>());
Does this approach even work? Is it even possible to activate a dynamic model in the new odata preview? or does anybody has a slight idea how to approach a dynamic routing for the new odata 8.0?
There is an example for dynamic routing and dynamic model here:
https://github.com/OData/AspNetCoreOData/blob/master/sample/ODataDynamicModel/
See MyODataRoutingApplicationModelProvider and MyODataRoutingMatcherPolicy which will pass a custom IEdmModel to the controller.
The HandleAllController can handle different types and edm models in a dynamic way.
So after 5 days of internal OData debugging I managed to get it to work. Here are the necessary steps:
First remove all OData calls/attributes from your controller/configure services which might do funky stuff (ODataRoutingAttribute or AddOData())
Create a simple asp.net controller with the route to your liking and map it in the endpoints
[ApiController]
[Route("odata/v{version}/{Path?}")]
public class HandleAllController : ControllerBase { ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
}
}
Create and register your InputFormatWrapper and OutputFormatWrapper
public class ConfigureMvcOptionsFormatters : IConfigureOptions<MvcOptions>
{
private readonly IServiceProvider _services;
public ConfigureMvcOptionsFormatters(IServiceProvider services)
{
_services = services;
}
public void Configure(MvcOptions options)
{
options.InputFormatters.Insert(0, new ODataInputFormatWrapper(_services));
options.OutputFormatters.Insert(0, new OdataOutputFormatWrapper(_services));
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.ConfigureOptions<ConfigureMvcOptionsFormatters>();
...
}
public class ODataInputFormatWrapper : InputFormatter
{
private readonly IServiceProvider _serviceProvider;
private readonly ODataInputFormatter _oDataInputFormatter;
public ODataInputFormatWrapper(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
//JSON and default is first - see factory comments
_oDataInputFormatter = ODataInputFormatterFactory.Create().First();
}
public override bool CanRead(InputFormatterContext context)
{
if (!ODataWrapperHelper.IsRequestValid(context.HttpContext, _serviceProvider))
return false;
return _oDataInputFormatter.CanRead(context);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return _oDataInputFormatter!.ReadRequestBodyAsync(context);
}
}
// The OutputFormatWrapper looks like the InputFormatWrapper
Within the ODataWrapperHelper you can check stuff and get/set your dynamic edmModel. It is necessary in the end to set these ODataFeature()... That's not beautiful but it gets the dynamic job done...
public static bool IsRequestValid(HttpContext context, IServiceProvider serviceProvider)
{
//... Do stuff, get datasource
var edmModel = dataSource!.GetModel();
var oSegment = new EntitySetSegment(new EdmEntitySet(edmModel.EntityContainer, targetEntity, edmModel.SchemaElements.First(x => targetEntity == x.Name) as EdmEntityType));
context.ODataFeature().Services = serviceProvider.CreateScope().ServiceProvider;
context.ODataFeature().Model = edmModel;
context.ODataFeature().Path = new ODataPath(oSegment);
return true;
}
Now to the ugly stuff: We still need to register some ODataService in ConfigureServices(IServiceCollection services). I added a function there called AddCustomODataService(services) and there you can either register ~40 services yourself or do some funky reflection...
So maybe if someone from the odata team reads this, please consider opening Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions
I created a
public class CustomODataServiceContainerBuilder : IContainerBuilder which is a copy of the internal Microsoft.AspNetCore.OData.Abstracts.DefaultContainerBuilder there I added the function:
public void AddServices(IServiceCollection services)
{
foreach (var service in Services)
{
services.Add(service);
}
}
and the ugly AddCustomODataServices(IServiceCollection services)
private void AddCustomODataService(IServiceCollection services)
{
var builder = new CustomODataServiceContainerBuilder();
builder.AddDefaultODataServices();
//AddDefaultWebApiServices in ContainerBuilderExtensions is internal...
var addDefaultWebApiServices = typeof(ODataFeature).Assembly.GetTypes()
.First(x => x.FullName == "Microsoft.AspNetCore.OData.Abstracts.ContainerBuilderExtensions")
.GetMethods(BindingFlags.Static|BindingFlags.Public)
.First(x => x.Name == "AddDefaultWebApiServices");
addDefaultWebApiServices.Invoke(null, new object?[]{builder});
builder.AddServices(services);
}
Now the controller should work again (with odataQueryContext and serialization in place) - Example:
[HttpGet]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
//... get model and entitytype
var queryContext = new ODataQueryContext(model, entityType, null);
var queryOptions = new ODataQueryOptions(queryContext, Request);
return (Collection<IEdmEntityObject>)myCollection;
}
[HttpPost]
public Task<IActionResult> Post([FromBody] IEdmEntityObject entityDataObject, CancellationToken cancellationToken)
{
//Do something with IEdmEntityObject
return Ok()
}

Model validation for configuration objects in ASP.NET Core

In .NET Core, if my appsettings file looks like
{
"Section": {
"Field": "value"
}
}
I can create a class like
public class Section
{
public string Field { get; set; }
}
and retrieve the value in Startup like
public void ConfigureServices(IServiceCollection services) {
services.Configure<Section>(this.Configuration.GetSection("Section"));
}
The problem is that if for some reason (say misspelling) the binding fails, it is not going to throw, and instead it will create a Section object with null (default) value for the Field property.
Is there a way to make services.Configure<Section>(this.Configuration.GetSection("Section")); to throw if the binding fails?
I am just summing up #Nkosi's answer here which makes this validation possible using data annotation.
1- Annotate the properties of your class:
public class Section
{
[Required]
public string Field { get; set; }
}
2- Create an extension method to enable validation to take effect:
public static class ConfigurationModelValidation
{
public static T GetValid<T>(this IConfiguration configuration)
{
var obj = configuration.Get<T>();
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
}
3- In the Startup class, register you configuration models as below using GetValid method (instead of using 'IOptions'):
public void ConfigureServices(IServiceCollection services) {
services.AddSingleton(this.Configuration.GetSection("Section").GetValid<Section>());
}
4- Now in the user's class directly inject your configuration model:
public class MyClass
{
private readonly string field;
public MyClass(Section section)
{
this.field = section.field;
}
}
Now if binding fails for any reason, the validation will kick in and it will throw, enjoy!
You can just get the section first, then verify it exists (because it will not be null).
public void ConfigureServices(IServiceCollection services) {
var section = this.Configuration.GetSection(nameof(Section));
if (!section.Exists()) throw new Exception();
services.Configure<Section>(section);
}

Trying to get injected service for seeding data in ASP.NET core MVC

In ConfigureServices I say
services.AddDbContext<PwdrsDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("PwdrsDbConnection"));
});
RegisterServices(services);
In configure I say
SeedData.SeedDatabase(app);
In the static seed method I say
public class SeedData
{
public static void SeedDatabase(IApplicationBuilder app)
{
PwdrsDbContext context = app.ApplicationServices.GetService<PwdrsDbContext>();
}
}
and when I run it says
Cannot resolve scoped service 'Pwdrs.Infra.Data.Context.PwdrsDbContext' from root provider
I need the dbcontext to seed the data but what am I missing?
You need to inject the services from your ConfigureServices method into the Configure method separately:
public void Configure(
IApplicationBuilder app,
IServiceProvider services) // <- magic here
{
// ...
SeedData.SeedDatabase(services);
}
public class SeedData
{
public static void SeedDatabase(IServiceProvider services)
{
PwdrsDbContext context = services.GetRequiredService<PwdrsDbContext>();
}
}

ServiceCollection configuration using strings (config files) in .NET Core

Is there a way to configure dependency injection in the standard Microsoft.Extensions.DependencyInjection.ServiceCollection library in .net core, without actually having a reference to the implementation classes in question? (to get implementation class names from configuration files?)
For example:
services.AddTransient<ISomething>("The.Actual.Thing");// Where The.Actual.Thing is a concrete class
If your really keen to use strings parameters to load objects on the fly, you can use a factory that creates dynamic objects.
public interface IDynamicTypeFactory
{
object New(string t);
}
public class DynamicTypeFactory : IDynamicTypeFactory
{
object IDynamicTypeFactory.New(string t)
{
var asm = Assembly.GetEntryAssembly();
var type = asm.GetType(t);
return Activator.CreateInstance(type);
}
}
Lets say you have the following service
public interface IClass
{
string Test();
}
public class Class1 : IClass
{
public string Test()
{
return "TEST";
}
}
You can then
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IDynamicTypeFactory, DynamicTypeFactory>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IDynamicTypeFactory dynamicTypeFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
var t = (IClass)dynamicTypeFactory.New("WebApplication1.Class1");
await context.Response.WriteAsync(t.Test());
});
}

Categories

Resources