2019 FILESTREAM: Descargar y cargar documentos desde SQL Server a través de ASP.Net MVC ISGCAS: ADMINISTRACIÓN DOCUMENTAL EN SQL SERVER OSCAR PIÑONES SAAVEDRA OPENDAT S.A. | Uribe 636, Oficina 601 Descargar y cargar medios de SQL Server a través de ASP.Net MVC Una pregunta frecuente que aparece en los foros de discusión es cómo servir un medio que se almacena en la tabla de SQL Server desde una aplicación ASP.Net. Desafortunadamente, la respuesta casi siempre es errónea, ya que la solución predominante consiste en copiar el medio completo. Esta solución funciona bien cuando se prueba con una carga ligera y devuelve pocos medios pequeños. Pero en el entorno de producción, la memoria requerida por todos los archivos almacenados como matrices de bytes en la memoria causa una grave degradación del rendimiento. Una buena solución debe utilizar la semántica de transmisión, transfiriendo los datos en pequeños fragmentos del servidor SQL al resultado devuelto de HTTP. Los componentes de SqlClient ofrecen semánticas de transmisión para grandes conjuntos de resultados, incluidos grandes campos BLOB, pero el cliente tiene que solicitarlo. El 'ingrediente secreto' es el paso en el indicador CommandBehavior.SequentialAccess hacia SqlCommand.ExecuteReader: Proporciona una forma para que el DataReader maneje las filas que contienen columnas con grandes valores binarios. En lugar de cargar toda la fila, SequentialAccess permite al DataReader cargar datos como una secuencia. Luego puede usar el método GetBytes o GetChars para especificar una ubicación de byte para iniciar la operación de lectura y un tamaño de búfer limitado para los datos que se devuelven. Una carpeta ASP.Net Virtual MVC Media respaldada por SQL Server Digamos que queremos tener una carpeta de medios virtual en un sitio MVC de ASP.Net, que sirva los archivos de una base de datos de SQL Server. Una solicitud GET para una URL como "http: //site/Media/IMG0042.JPG" debe devolver el contenido del archivo llamado IMG0042.JPG de la base de datos. Una solicitud POST a la URL "http: // site / Media" que contiene un archivo incrustado debe insertar este nuevo archivo en la base de datos y redirigir la respuesta a la ruta virtual del archivo recién agregado. Así es como subimos el formulario HTML: <form method="post" action="/Media" enctype="multipart/form-data"> <input type="file" name="file" id="file"/> <input type="submit" name="Submit" value="Submit"/> </form> routes.MapRoute( "Media", "Media/{filename}", new { controller = "Media", action = "GetFile" }, new { filename = @"[^/?*:;{}\\]+" }); 26-5-2019 Comenzaremos agregando una ruta especial en Global.asax.cs que actuará como una carpeta virtual para las solicitudes de descarga de archivos: 1 Tenga en cuenta que la carga será manejada por la ruta MVC predeterminada si agregamos un método Index() a nuestro controlador que manejará el POST. Para nuestras solicitudes GET necesitamos una clase FileDownloadModel para representar las propiedades del archivo solicitado. Para este ejemplo, no necesitamos un modelo POST ya que solo tendremos un único campo de entrada, el archivo cargado. Vamos a utilizar una interfaz de repositorio que declara dos métodos: GetFileByName devuelve desde el repositorio un FileDownloadModel dado un nombre de archivo, y PutFile aceptará un archivo cargado y lo colocará en el repositorio. public class FileDownloadModel { public public public public public string FileName {get; internal set;} string ContentType {get; internal set;} string ContentCoding {get; internal set;} long ContentLength {get; internal set;} Stream Content {get; internal set;} } public interface IMediaRepository { bool GetFileByName( string fileName, out FileDownloadModel file); void PostFile(HttpPostedFileBase file, out string fileName); } Con esta interfaz de Repository podemos codificar nuestra clase MediaController: public class MediaController: Controller { public IMediaRepository Repository {get; set;} public MediaController() { Repository = new SqlMediaRepository(); } [HttpGet] public ActionResult GetFile(string fileName) { FileDownloadModel model; if (false == Repository.GetFileByName( fileName, out model)) { return new HttpNotFoundResult { StatusDescription = String.Format( "File {0} not found", fileName) 26-5-2019 [HttpPost] public ActionResult Index() { string fileName; Repository.PostFile(Request.Files[0], out fileName); return new RedirectResult("/Media/" + fileName); } 2 }; } if ( null != model.ContentCoding) { Response.AddHeader( "Content-Encoding", model.ContentCoding); } Response.AddHeader( "Content-Length", model.ContentLength.ToString ()); Response.BufferOutput = false; return new FileStreamResult( model.Content, model.ContentType); } } Hemos codificado la implementación del Repositorio en SqlMediaRepository, una clase que crearemos en breve. Un proyecto real probablemente usaría los patrones de inyección de dependencia o inversión de control, tal vez usando Castle Windsor, por ejemplo. Para mayor brevedad, omitiremos estos detalles, hay muchos blogs y artículos que describen cómo hacerlo. Tenga en cuenta el uso de la devolución FileStreamResult, que es una acción suministrada por MVC para devolver una descarga de un objeto Stream arbitrario. Lo que también nos lleva al siguiente punto, necesitamos implementar un flujo que lea el contenido de un SqlDataReader. Una secuencia basada en SqlDataReader Ahora necesitamos una implementación de la clase Stream abstracta que pueda transmitir una columna BLOB desde un SqlDataReader. Espera, dices, ¿SqlBytes ya tiene una propiedad Stream que lee un BLOB de un resultado como un Stream? Desafortunadamente, este pequeño comentario hace que esta clase sea inútil para nuestros propósitos: Obtener o configurar la propiedad Stream carga todos los datos en la memoria. Su uso con datos de gran tamaño puede provocar una excepción OutOfMemoryException. public class SqlReaderStream: Stream { private SqlDataReader reader; private int columnIndex; 26-5-2019 Así que nos quedamos con la implementación de un Stream basado en un campo BLOB SqlDataReader, un Stream que devuelve el contenido del BLOB utilizando las llamadas GetBytes adecuadas y no carga todo el BLOB en la memoria. Afortunadamente, esto es bastante simple ya que solo necesitamos implementar un puñado de métodos: 3 private long position; public SqlReaderStream( SqlDataReader reader, int columnIndex) { this.reader = reader; this.columnIndex = columnIndex; } public override long Position { get {return position;} set {throw new NotImplementedException();} } public override int Read(byte[] buffer, int offset, int count) { long bytesRead = reader.GetBytes(columnIndex, position, buffer, offset, count); position += bytesRead; return (int)bytesRead; } public override bool CanRead { get {return true;} } public override bool CanSeek { get {return false;} } public override bool CanWrite { get {return false;} } public override void Flush() { throw new NotImplementedException(); } public override long Length { get {throw new NotImplementedException();} } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } protected override void Dispose(bool disposing) { if (disposing && null != reader) { reader.Dispose(); reader = null; } base.Dispose(disposing); } 26-5-2019 public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } 4 } Como puede ver, solo necesitamos devolver las respuestas correctas a CanRead (sí), CanWrite (no) y CanSeek (también no), realizar un seguimiento de nuestra posición actual y necesitamos implementar la lectura para obtener más bytes del lector, usando GetReader. También estamos anulando el método Dispose (bool disposing). Esto se debe a que tendremos que cerrar el SqlDataReader cuando se complete la transferencia de contenido. Si es la primera vez que ve esta firma del método de Disposición, entonces debe leer Implementación de un método de Disposición Streaming de carga de datos BLOB Al igual que recuperar datos BLOB grandes de SQL Server presenta desafíos para evitar la creación de copias completas en memoria de todo el BLOB, surgen problemas similares cuando se intenta insertar un BLOB. La mejor solución es en realidad bastante complicada. Implica enviar los datos al servidor en trozos y usar la sintaxis de BLOB UPDATE en el lugar. La MSDN tiene esto que decir en la sección de Comentarios: Use la cláusula .WRITE (expresión, @Offset, @Length) para realizar una actualización parcial o completa de los tipos de datos varchar (max), nvarchar (max) y varbinary (max). Por ejemplo, una actualización parcial de una columna varchar (max) podría eliminar o modificar solo los primeros 200 caracteres de la columna, mientras que una actualización completa eliminaría o modificaría todos los datos de la columna. Para obtener el mejor rendimiento, recomendamos que los datos se inserten o actualicen en tamaños de trozos que sean múltiplos de 8040 bytes. Para implementar dicha semántica, escribiremos una segunda implementación de Stream, esta vez para las subidas: public class SqlStreamUpload: Stream { public SqlCommand InsertCommand {get; set;} public SqlCommand UpdateCommand {get; set;} public SqlParameter InsertDataParam {get; set;} public SqlParameter UpdateDataParam {get; set;} public override bool CanRead { get {return false;} } public override bool CanSeek { get {return false;} } public override void Flush() { } public override long Length { get {throw new NotImplementedException();} 26-5-2019 public override bool CanWrite { get {return true;} } 5 } public override long Position { get; set; } public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { byte[] data = buffer; if (offset != 0 || count != buffer.Length) { data = new byte[count]; Array.Copy(buffer, offset, data, 0, count); } if (0 == Position && null != InsertCommand) { InsertDataParam.Value = data; InsertCommand.ExecuteNonQuery(); } else { UpdateDataParam.Value = data; UpdateCommand.ExecuteNonQuery(); } Position += count; } } Esta implementación de Stream utiliza dos objetos SqlCommand: un InsertCommand para guardar el primer fragmento, y un UpdateCommand para guardar los fragmentos subsiguientes. Tenga en cuenta que el tamaño de los fragmentos (los 8040 bytes óptimos) no se especifica en ninguna parte, lo que se logra fácilmente envolviendo el SqlStreamUpload en una instancia de BufferedStream. create table media ( [media_id] int not null identity(1,1), [file_name] varchar(256), [content_type] varchar(256), [content_coding] varchar(256), [content] varbinary(max), constraint pk_media_id primary key([media_id]), constraint unique_file_name unique ([file_name])); 26-5-2019 La tabla MEDIA 6 Esta tabla contiene los archivos multimedia descargables. Los archivos se identifican por su nombre, por lo que los nombres tienen una restricción única. He agregado una clave primaria de IDENTIDAD, porque en un CMS estos archivos a menudo son referencias de otras partes de la aplicación e INT es una clave de referencia más corta que un nombre de archivo. El campo tipo de contenido es necesario para saber el tipo de archivo: "imagen / png", "imagen / jpg", etc. (consulte los tipos de medios de la IANA registrados oficialmente. El campo de codificación de contenido es necesario si los archivos se almacenan comprimidos y un HTTP de codificación de contenido El encabezado con la etiqueta "gzip" o "desinflar" se debe agregar a la respuesta. Tenga en cuenta que la mayoría de los tipos de imágenes (JPG, PNG) no se comprimen bien, ya que estos formatos de archivo ya incluyen un algoritmo de compresión y cuando se comprimen nuevamente con el los algoritmos de transferencia HTTP comunes (gzip, deflate, comprimir) generalmente aumentan de tamaño. El SqlMediaRepository La última pieza del rompecabezas: una implementación de la interfaz IMediaRepository que utiliza un servidor de SQL Server para el almacenamiento de archivos: public class SqlMediaRepository: IMediaRepository { private SqlConnection GetConnection() { SqlConnectionStringBuilder scsb = new SqlConnectionStringBuilder( ConfigurationManager.ConnectionStrings["Images"].ConnectionString); scsb.Pooling = true; SqlConnection conn = new SqlConnection(scsb.ConnectionString); conn.Open(); return conn; } public void PostFile( HttpPostedFileBase file, out string fileName) { fileName = Path.GetFileName(file.FileName); SqlCommand cmdUpdate = new SqlCommand( @"UPDATE media SET content.write (@data, NULL, NULL) WHERE file_name = @content_disposition;", conn, trn); cmdUpdate.Parameters.Add("@data", SqlDbType.VarBinary, -1); 26-5-2019 using (SqlConnection conn = GetConnection()) { using (SqlTransaction trn = conn.BeginTransaction()) { SqlCommand cmdInsert = new SqlCommand( @"INSERT INTO media ( file_name, content_type, content_coding, content) values ( @content_disposition, @content_type, @content_coding, @data);", conn, trn); cmdInsert.Parameters.Add("@data", SqlDbType.VarBinary, -1); cmdInsert.Parameters.Add("@content_disposition", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_disposition"].Value = fileName; cmdInsert.Parameters.Add("@content_type", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_type"].Value = file.ContentType; cmdInsert.Parameters.Add("@content_coding", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_coding"].Value = DBNull.Value; 7 cmdUpdate.Parameters.Add("@content_disposition", SqlDbType.VarChar, 256); cmdUpdate.Parameters["@content_disposition"].Value = fileName; using (Stream uploadStream = new BufferedStream( new SqlStreamUpload { InsertCommand = cmdInsert, UpdateCommand = cmdUpdate, InsertDataParam = cmdInsert.Parameters["@data"], UpdateDataParam = cmdUpdate.Parameters["@data"] }, 8040)) { file.InputStream.CopyTo(uploadStream); } trn.Commit(); } } } public bool GetFileByName(string fileName, out FileDownloadModel file) { SqlConnection conn = GetConnection(); try { SqlCommand cmd = new SqlCommand( @"SELECT file_name, content_type, content_coding, DATALENGTH (content) as content_length, content FROM media WHERE file_name = @fileName;", conn); SqlParameter paramFilename = new SqlParameter( @"fileName", SqlDbType.VarChar, 256); paramFilename.Value = fileName; cmd.Parameters.Add(paramFilename); SqlDataReader reader = cmd.ExecuteReader( CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow | CommandBehavior.CloseConnection); if (false == reader.Read()) { reader.Dispose(); conn = null; file = null; return false; } string contentDisposition = reader.GetString(0); string contentType = reader.GetString(1); string contentCoding = reader.IsDBNull(2) ? null : reader.GetString(2); long contentLength = reader.GetInt64(3); Stream content = new SqlReaderStream(reader, 4); } finally { if (null != conn) { conn.Dispose(); } 26-5-2019 file = new FileDownloadModel { FileName = contentDisposition, ContentCoding = contentCoding, ContentType = contentType, ContentLength = contentLength, Content = content }; conn = null; // ownership transfered to the reader/stream return true; 8 } } } Normalmente, las aplicaciones MVC tienden a usar un repositorio basado en LINQ. En este caso, aunque no pude aprovechar LINQ debido a los requisitos peculiares de la implementación de transmisión eficiente para BLOB grandes. Por lo tanto, este Repositorio usa código SqlClient simple de vainilla. El método GetFileByName obtiene una fila de la tabla de medios y devuelve un FileDownloadModel con un objeto SqlReaderStream que está envolviendo el resultado del comando SELECT. Tenga en cuenta que no pude implementar el patrón típico de "uso" para la SqlConnection desechable porque la conexión debe permanecer abierta hasta que el comando SqlDataReader termine la transmisión en el BLOB, y la secuencia será controlada por el marco MVC que ejecuta ActionResult después de que GetFileByName sea terminado. La conexión se cerrará cuando se elimine el SqlReaderStream, debido a la bandera CommandBehavior.CloseConnection. La secuencia será eliminada por el método FileStreamResult.WriteFile. El método PostFile crea dos instrucciones SqlCommand, una para insertar la primera parte de los datos junto con las otras columnas relevantes, y un comando de actualización que usa actualizaciones parciales de BLOB para escribir las partes posteriores. Un objeto SqlStreamUpload está utilizando estos dos SqlCommand para transmitir de manera eficiente en el archivo cargado. El BufferedStream intermedio se utiliza para crear fragmentos de carga del tamaño crítico de 8040 bytes (ver más arriba). Si hubiera que comprimir el contenido, aquí es donde sucedería, se colocaría un GZipStream frente a Bufferedstream para comprimir el archivo cargado, y el parámetro "@content_coding" tendría que configurarse de modo "gzip”. Almacenamiento en caché HTTP He dejado fuera de esta implementación el control de almacenamiento en caché HTTP apropiado. El almacenamiento en caché de HTTP es extremadamente importante en los sitios de alto volumen de tráfico, no hay mejor manera de optimizar el procesamiento de una solicitud HTTP que nunca reciba dicha solicitud y que la memoria caché de usuario-agente o algún proxy intermedio sirvan una respuesta. Nuestra aplicación tendría que agregar los encabezados HTTP apropiados de ETag y Cache-Control y el MediaController tendría que tener una acción para las solicitudes HEAD. La interfaz de IMediaRepository necesitaría un nuevo método para obtener todas las propiedades del archivo sin el contenido real. Por el momento, dejaré esto como un ejercicio para el lector ... Este artículo no trata de abordar la pregunta fundamental de si debe almacenar las imágenes en la base de datos o en el sistema de archivos. Los argumentos pueden ser a favor y en contra de esto. Russel Sears, Catherine van Ingen y Jim Gray publicaron un trabajo de investigación en 2006 sobre su análisis de comparación de rendimiento entre el almacenamiento de archivos en la base de datos y el almacenamiento en el sistema de archivos: A BLOB or Not a BLOB: Almacenamiento de objetos grandes en una Base de datos o Sistema de archivos ?. Llegaron a la conclusión de que: 26-5-2019 BLOB o no BLOB 9 El estudio indica que si los objetos son más grandes que un megabyte en promedio, NTFS tiene una clara ventaja sobre SQL Server. Si los objetos tienen menos de 256 kilobytes, la base de datos tiene una clara ventaja. Dentro de este rango, depende de la intensidad de escritura de la carga de trabajo y de la antigüedad de almacenamiento de una réplica típica en el sistema. 26-5-2019 Sin embargo, su estudio no comparaba cómo servir los archivos como respuestas HTTP. El hecho de que el servidor web pueda servir un archivo de manera eficiente directamente desde el sistema de archivos sin que se ejecute ningún código en la aplicación web cambia la ecuación bastante, e inclina el rendimiento fuertemente a favor del sistema de archivos. Pero si ya ha considerado los pros y los contras y ha decidido que las ventajas de una copia de seguridad / restauración consistente y una integridad referencial sólida ameritan BLOB almacenados en la base de datos, espero que este artículo resalte una manera eficiente de devolver esos BLOB como respuestas HTTP. 10 FILESTREAM MVC: descarga y carga multimedial de SQL Server En el artículo anterior, se ha mostrado cómo es posible usar una semántica de transmisión eficiente al descargar y cargar imágenes desde SQL Server a través de ASP.Net MVC. En este artículo repasaremos un enfoque alternativo que se basa en los tipos de columna FILESTREAM introducidos en SQL Server 2008. ¿Qué es FILESTREAM? El almacenamiento FILESTREAM es una nueva opción disponible en SQL Server 2008 y versiones posteriores que permite que las columnas BLOB se almacenen directamente en el sistema de archivos como archivos individuales. Como archivos, los datos son accesibles a través de la API de acceso a archivos Win32 como ReadFile y WriteFile. Pero al mismo tiempo, los mismos datos están disponibles a través de las operaciones normales de T-SQL como SELECT o UPDATE. No solo eso, sino que los datos están contenidos de forma lógica en la base de datos, por lo que se incluirán en una copia de seguridad de la base de datos, están sujetos a un comportamiento de confirmación y retrotracción de transacciones ordinarios, se buscan en los índices de texto completo de SQL Server y siguen la seguridad normal de SQL Server. Reglas de acceso: si se le otorga el permiso SELECCIONAR en la tabla, puede abrir el archivo para leer. Hay algunas restricciones, por ejemplo. una base de datos con FILESTREAM no puede ser duplicada. Para obtener una lista completa de restricciones y limitaciones, consulte Uso de FILESTREAM con otras características de SQL Server. Tenga en cuenta que la edición de SQL Server Express no admite el almacenamiento de FILESTREAM. Otro atributo del almacenamiento de FILESTREAM es la limitación de tamaño: los valores de columna BLOB normales tienen un tamaño máximo de 2Gb. Las columnas FILESTREAM están limitadas solo por el límite de tamaño de volumen del sistema de archivos. 2Gb puede parecer un gran valor, pero tenga en cuenta que un archivo multimedia como una secuencia de películas HD puede subir fácilmente a un tamaño de 5Gb. Utilizando FILESTREAM Una forma de utilizar las columnas FILESTREAM es tratarlas como valores BLOB ordinarios y manipularlos a través de T-SQL. La única restricción es que no se admite la sintaxis de actualización parcial eficiente para BLOB. Es decir, no se puede ejecutar UPDATE tabla SET columna. WRITE(…) WHERE … en una columna FILESTREAM. Pero cuando el almacenamiento de FILESTREAM comienza a brillar es cuando se accede a través de la API del sistema de archivos. Esto permite que la aplicación lea, escriba y busque de manera eficiente en un valor BLOB grande, tal como lo haría en un archivo. De hecho, la aplicación sí lee, escribe y busca en un archivo :). El requisito de proporcionar un contexto de transacción al manipular un valor de FILESTREAM a través del archivo IO API destaca otro aspecto del trabajo con este nuevo tipo: una transacción T-SQL debe iniciarse y mantenerse abierta mientras se manipula el archivo, y luego debe estar comprometido Si lo piensa, tal requisito es de esperar, ya que dijimos que las columnas FILESTREAM están sujetas a la semántica normal de confirmación y retrotracción 26-5-2019 Las aplicaciones nativas de Win32 usan una nueva función de API OpenSqlFilestream que abre un HANDLE que luego se puede usar con las funciones de la API de IO del archivo. Las aplicaciones administradas utilizan la nueva clase SqlFileStream que expone una secuencia basada en el valor de FILESTREAM subyacente. Tanto la API nativa como la administrada requieren como entrada dos valores especiales, un Nombre de ruta y un Contexto de transacción que deben obtenerse previamente del servidor SQL utilizando T-SQL. 11 de transacciones de T-SQL, incluso cuando se manipulan mediante la API de lectura / escritura de archivos de Windows. Tabla de multimedios basada en FILESTREAM Para tener la columna FILESTREAM, debemos tener un grupo de archivos especial en nuestra base de datos, un grupo de archivos FILESTREAM. Puede agregar un nuevo grupo de archivos a su base de datos existente o puede crear la base de datos con un grupo de archivos FILESTREAM desde cero. Además, la función FILESTREAM debe estar habilitada en la instancia de SQL Server. Para todos los detalles, consulte Cómo comenzar con el almacenamiento de FILESTREAM. Para mi proyecto, simplemente voy a crear una nueva base de datos con un grupo de archivos FILESTREAM: create database images on (name='images_data', filename='c:\temp\images.mdf') , filegroup FS contains FILESTREAM (name = 'images_files', filename='c:\temp\images_files') log on (name='images_log', filename='c:\temp\images.ldf'); go La tabla de medios utilizada por nuestro proyecto MVC será similar a la utilizada en el artículo anterior, pero a la columna de contenido se le agregará el atributo FILESTREAM. Es necesario que una tabla que tenga columnas FILESTREAM tenga una columna ROWGUIDCOL, por lo que también agregaremos una de ellas: create table media ( [media_id] int not null identity(1,1), [file_name] varchar(256), [content_type] varchar(256), [content_coding] varchar(256), [media_rowguid] uniqueidentifier not null ROWGUIDCOL UNIQUE default newsequentialid(), [content] varbinary(max) filestream, constraint pk_media_id primary key([media_id]), constraint unique_file_name unique ([file_name])); go Repositorio basado en FILESTREAM /// <summary> /// SQL Server FILESTREAM based implementation of the IMediaRepository /// </summary> public class FileStreamMediaRepository: IMediaRepository { /// <summary> /// Gets an open connection to the SQL Server back end /// </summary> /// <returns>the SqlConneciton object, ready to use</returns> 26-5-2019 Si aún no ha leído el artículo anterior Descargar y cargar imágenes de SQL Server a través de ASP.Net MVC, ahora es un buen momento para hacerlo. Vamos a reutilizar el mismo código y simplemente proporcionaremos una nueva implementación para la interfaz IMediaRepository, una implementación que funciona con el almacenamiento de FILESTREAM: 12 private SqlConnection GetConnection() { SqlConnectionStringBuilder scsb = new SqlConnectionStringBuilder( ConfigurationManager.ConnectionStrings["Images"].ConnectionString); scsb.Pooling = true; scsb.AsynchronousProcessing = true; SqlConnection conn = new SqlConnection(scsb.ConnectionString); conn.Open(); return conn; } /// <summary> /// Gets a file from the SQL repository /// </summary> /// <param name="fileName">filename to retrieve</param> /// <param name="file">Output, the model for the file if found</param> /// <returns>True if the file is found, False if not</returns> public bool GetFileByName(string fileName, out FileDownloadModel file) { SqlConnection conn = GetConnection(); SqlTransaction trn = conn.BeginTransaction(); try { SqlCommand cmd = new SqlCommand( @"SELECT file_name, content_type, content_coding, DATALENGTH (content) as content_length, content.PathName() as path, GET_FILESTREAM_TRANSACTION_CONTEXT () FROM media WHERE file_name = @fileName;", conn, trn); SqlParameter paramFilename = new SqlParameter( @"fileName", SqlDbType.VarChar, 256); paramFilename.Value = fileName; cmd.Parameters.Add(paramFilename); using (SqlDataReader reader = cmd.ExecuteReader()) { if (false == reader.Read()) { reader.Close(); trn.Dispose(); conn.Dispose(); trn = null; conn = null; file = null; return false; } file = new FileDownloadModel { FileName = contentDisposition, ContentCoding = contentCoding, ContentType = contentType, ContentLength = contentLength, Content = new MvcResultSqlFileStream { SqlStream = new SqlFileStream(path, context, FileAccess.Read), Connection = conn, Transaction = trn } }; conn = null; // ownership transfered to the stream trn = null; 26-5-2019 string contentDisposition = reader.GetString(0); string contentType = reader.GetString(1); string contentCoding = reader.IsDBNull(2) ? null: reader.GetString(2); long contentLength = reader.GetInt64(3); string path = reader.GetString(4); byte[] context = reader.GetSqlBytes(5).Buffer; 13 return true; } } finally { if (null != trn) { trn.Dispose(); } if (null != conn) { conn.Dispose(); } } } /// <summary> /// Adds a file to the SQL repository /// </summary> /// <param name="file">POST-ed file to be added</param> /// <param name="fileName">The filename part of the uploaded file public void PostFile(HttpPostedFileBase file, out string fileName) { fileName = Path.GetFileName(file.FileName); path</param> using (SqlConnection conn = GetConnection ()) { using (SqlTransaction trn = conn.BeginTransaction ()) { SqlCommand cmdInsert = new SqlCommand( @"insert into media (file_name, content_type, content_coding, content) output INSERTED.content.PathName(), GET_FILESTREAM_TRANSACTION_CONTEXT () values (@content_disposition, @content_type, @content_coding, 0x)", conn, trn); cmdInsert.Parameters.Add("@content_disposition", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_disposition"].Value = fileName; cmdInsert.Parameters.Add("@content_type", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_type"].Value = file.ContentType; cmdInsert.Parameters.Add("@content_coding", SqlDbType.VarChar, 256); cmdInsert.Parameters["@content_coding"].Value = DBNull.Value; string path = null; byte[] context = null; // cmdInsert is an INSERT command that uses the OUTPUT clause // Thus we use the ExecuteReader to get the // result set from the output columns // using (SqlDataReader rdr = cmdInsert.ExecuteReader()) { rdr.Read(); path = rdr.GetString(0); context = rdr.GetSqlBytes(1).Buffer; } using (SqlFileStream sfs = new SqlFileStream( path, context, FileAccess.Write)) { file.InputStream.CopyTo(sfs); } trn.Commit (); } } } 26-5-2019 } 14 La implementación es bastante sencilla. El método PostFile inicia una transacción, inserta una fila en la tabla de imágenes y obtiene el contexto de transacción PahtName y FILESTREAM en la fila recién insertada, y luego abre un SqlFileStream y copia en el archivo cargado directamente en el archivo que respalda la columna FILESTREAM. El método GetFileByName obtiene el contexto de transacción PathName y FILESTREAM del archivo deseado y luego devuelve un SqlFileStream que accede directamente al archivo en el que se almacena el valor de FILESTREAM. Lo único que requiere explicación es la clase MvcResultSqlFileStream. Debido a que SqlFileStream usa un contexto de transacción, se requiere que mantenga SqlConnection y SqlTransaction abiertos hasta que SqlFileStream termine de acceder al contenido. En el método PostFile, este requisito se logra fácilmente porque todo el uso de SqlFileStream se completa dentro del marco de la pila, pero el método GetFileByName tiene que devolver un Stream que a su vez se pasa a un FileStreamResult por la clase MediaController y no hay control sobre cómo El marco MVC utilizará este ActionResult devuelto. Mi solución es envolver el SqlFileStream en una nueva clase derivada de Stream (ya que SqlFileStream está sellado, no se puede heredar ...) y luego manejar la administración de recursos en esta clase derivada ’ /// <summary> /// Implementation of System.IO.Stream based on a SqlFileStream /// Disposes the connection, transaction and SqlFileStream objects /// </summary> public class MvcResultSqlFileStream: Stream { public SqlFileStream SqlStream { get; set; } public SqlConnection Connection { get; set; } public SqlTransaction Transaction { get; set; } public override bool CanRead { get { return SqlStream.CanRead; } } public override bool CanSeek { get { return SqlStream.CanSeek; } } public override bool CanWrite { get { return SqlStream.CanWrite; } } public override void Flush() { SqlStream.Flush (); } public override long Position { get { return SqlStream.Position; } set { SqlStream.Position = value; } } public override int Read(byte[] buffer, int offset, int count) { 26-5-2019 public override long Length { get { return SqlStream.Length; } } 15 return SqlStream.Read (buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) { return SqlStream.Seek(offset, origin); } public override void SetLength(long value) { SqlStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { SqlStream.Write(buffer, offset, count); } protected override void Dispose(bool disposing) { if (disposing) { SqlStream.Dispose(); Transaction.Dispose(); Connection.Dispose(); } base.Dispose(disposing); } } Esta clase adicional se siente más bien innecesaria, pero desafortunadamente es necesaria porque el marco MVC no tiene un manejo adecuado de los recursos que pasan del controlador. Si nos preguntara, ActionResult debería implementar IDisposable, pero, por supuesto, nadie nos ha preguntado 😉 Conclusión El manejo del contenido multimedia almacenado en la base de datos mediante el almacenamiento de FILESTREAM hace que la experiencia del desarrollador sea más agradable. Se puede acceder al contenido de FILESTREAM utilizando la semántica de transmisión basada en la clase SqlFileStream o utilizando la API de E / S del archivo Win32 para aplicaciones nativas. En comparación, el artículo anterior que se basó en T-SQL directo para manipular los valores BLOB tuvo que hacer algunos trucos muy poco intuitivos para lograr la semántica de transmisión, especialmente para cargar un archivo. El almacenamiento FILESTREAM simplifica el desarrollo y es compatible con todas las ediciones de SQL Server, incluido Express. Si tiene la intención de implementar FILESTREAM en su entorno, se recomienda que revise primero algunos artículos de la Base de conocimientos y artículos técnicos: FILESTREAM Storage in SQL Server 2008 How to: Use FILESTREAM in a Firewall-Protected Environment Recommendations and guidelines for improving SQL Server FileStream performance 26-5-2019 16 FILESTREAM permite a las aplicaciones basadas en SQL Server almacenar datos no estructurados, como documentos e imágenes, en el sistema de archivos. FILESTREAM habilita a las aplicaciones basadas en SQL Server almacenar datos no estructurados, tales como documentos e imágenes, en el sistema de archivos. Las aplicaciones pueden aprovechar las API de transmisión de datos enriquecidas y el rendimiento del sistema de archivos al mismo tiempo que mantienen la coherencia transaccional entre los datos no estructurados y los datos estructurados correspondientes. FILESTREAM integra Motor de base de datos de SQL Server con sistemas de archivos NTFS o ReFS almacenando datos de objetos binarios grandes (BLOB) varbinary(max) como archivos en el sistema de archivos. Transact-SQL puede insertar, actualizar, consultar, buscar y realizar copias de seguridad de los datos FILESTREAM. Las interfaces del sistema de archivos de Win32 proporcionan el acceso de la transmisión por secuencias a los datos. FILESTREAM usa la memoria caché del sistema NT para almacenar en memoria caché los datos de archivos. Esto ayuda a reducir cualquier efecto que los datos FILESTREAM podrían tener en el rendimiento de motor de base de datos. No se usa el grupo de búferes de SQL Server; por consiguiente, esta memoria está disponible para el procesamiento de consultas. FILESTREAM no se habilita automáticamente al instalar o actualizar SQL Server. Debe habilitar FILESTREAM utilizando el Administrador de configuración de SQL Server y SQL Server Management Studio. Para utilizar FILESTREAM, debe crear o modificar una base de datos que contenga un tipo especial de grupo de archivos. Luego, debe crear o modificar una tabla de modo que contenga una columna varbinary(max) con el atributo FILESTREAM. Después de completar estas tareas, puede usar Transact-SQL y Win32 para administrar los datos FILESTREAM. Cuando se usa FILESTREAM En SQL Server, los BLOB pueden ser datos de varbinary(max) estándar que almacena los datos en tablas u objetos FILESTREAM varbinary(max) que almacenan los datos en el sistema de archivos. El tamaño y el uso de los datos determinan si debería usar el almacenamiento de base de datos o el almacenamiento del sistema de archivos. Si las condiciones siguientes son verdaderas, debería pensar en usar FILESTREAM: Los objetos que se están almacenando son, por término medio, mayores de 1 MB. El acceso de lectura rápido es importante. Está desarrollando aplicaciones que usan un nivel intermedio para la lógica de la aplicación. Para objetos de menor tamaño, el almacenamiento de BLOB varbinary(max) en la base de datos a menudo proporciona un mejor rendimiento de la transmisión de datos. Almacenamiento de FILESTREAM Para especificar que una columna debería almacenar datos en el sistema de archivos, especifique el atributo FILESTREAM en una columna varbinary(max). Esto hace que motor de base de datos almacene todos los datos para esa columna en el sistema de archivos pero no en el archivo de base de datos. 26-5-2019 El almacenamiento de FILESTREAM se implementa como una columna varbinary(max) en la que los datos están almacenados como BLOB en el sistema de archivos. Los tamaños de los BLOB están limitados solo por el tamaño del volumen del sistema de archivos. La limitación varbinary(max) estándar de tamaños de archivo de 2 GB no se aplica a BLOB que están almacenados en el sistema de archivos. 17 Los datos de FILESTREAM deben estar almacenados en los grupos de archivos FILESTREAM. Un grupo de archivos FILESTREAM es un grupo de archivos especial que contiene los directorios de sistema de archivos en lugar de los propios archivos. Estos directorios del sistema de archivos se denominan contenedores de datos. Los contenedores de datos son la interfaz entre el almacenamiento del motor de base de datos y el almacenamiento del sistema de archivos. Cuando use el almacenamiento FILESTREAM, piense en lo siguiente: Cuando una tabla contiene una columna FILESTREAM, cada fila debe tener un identificador de fila único distinto de NULL. Se pueden agregar varios contenedores de datos a un grupo de archivos FILESTREAM. Los contenedores de datos FILESTREAM no pueden estar anidados. Cuando se usan clústeres de conmutación por error, los grupos de archivos FILESTREAM deben estar en recursos de disco compartido. Los grupos de archivos FILESTREAM pueden estar en volúmenes comprimidos. Administración integrada Debido a que FILESTREAM se implementa como columna varbinary(max) y se integra directamente en el motor de base de datos, la mayoría de las funciones y de las herramientas de administración de SQL Server funcionan sin la modificación de los datos FILESTREAM. Por ejemplo, puede usar todos los modelos de recuperación y copia de seguridad con datos FILESTREAM y se realizan copias de seguridad de los datos FILESTREAM con los datos estructurados de la base de datos. Si no desea realizar una copia de seguridad de los datos FILESTREAM con datos relacionales, puede usar una copia de seguridad parcial para excluir los grupos de archivos FILESTREAM. Seguridad integrada En SQL Server, los datos de FILESTREAM se protegen de la misma manera que los demás datos: concediendo permisos en el nivel de tabla o columna. Si un usuario tiene permiso para la columna FILESTREAM de una tabla, el usuario puede abrir los archivos asociados. Nota El cifrado no se admite en los datos FILESTREAM. Solo la cuenta con la que la cuenta de servicio de SQL Server se ejecuta permiten los permisos al contenedor FILESTREAM. Recomendamos que no se concedan permisos a ninguna otra cuenta en el contenedor de datos. Nota 26-5-2019 Los inicios de sesión de SQL no funcionarán con contenedores FILESTREAM.SQL Solo la autenticación NTFS o ReFS funcionará con contenedores FILESTREAM. 18 Acceso a datos BLOB con Transact-SQL y acceso de transmisión de datos del sistema de archivos Después de almacenar los datos en una columna FILESTREAM, puede tener acceso a los archivos usando las transacciones de Transact-SQL o usando las API de Win32. Acceso a Transact-SQL Usando Transact-SQL, puede insertar, actualizar y eliminar los datos de FILESTREAM: Puede usar una operación de inserción para rellenar previamente un campo FILESTREAM con un valor nulo, un valor vacío o un dato insertado relativamente corto. Sin embargo, se envía una gran cantidad de datos de manera más eficaz en un archivo que usa interfaces de Win32. Al actualizar un campo FILESTREAM, modifica los datos de BLOB subyacentes en el sistema de archivos. Cuando un campo FILESTREAM está establecido en NULL, se eliminan los datos de BLOB asociados al campo. No puede usar ninguna actualización fragmentada de Transact-SQL, implementada como UPDATE. Write(), para realizar actualizaciones parciales en los datos. Al eliminar una fila, o eliminar o truncar una tabla que contiene datos FILESTREAM, elimina los datos de BLOB subyacentes del sistema de archivos. Acceso a la transmisión por secuencias del sistema de archivos La compatibilidad de transmisión por secuencias de Win32 funciona en el contexto de una transacción de SQL Server. Dentro de una transacción, puede usar las funciones FILESTREAM para obtener una ruta de acceso al sistema de archivos de UNC lógica de un archivo. Tras ello, use la API OpenSqlFilestream para obtener un identificador de archivos. Después, este identificador lo pueden usar las interfaces de transmisión por secuencias de archivo de Win32, como ReadFile() y WriteFile(), para obtener acceso y actualizar el archivo a través del sistema de archivos. Dado que las operaciones de archivo son transaccionales, no puede eliminar ni cambiar el nombre de los archivos FILESTREAM a través del sistema de archivos. Modelo de la instrucción El acceso del sistema de archivos de FILESTREAM modela una instrucción de Transact-SQL usando la apertura y el cierre de archivo. La instrucción se inicia cuando un identificador de archivos se abre y finaliza cuando se cierra el identificador. Por ejemplo, cuando se cierra un identificador de escritura, cualquier posible desencadenador de AFTER que esté registrado en la tabla se desencadena como si la instrucción UPDATE estuviera En FILESTREAM, el motor de base de datos controla el espacio de nombres del sistema de archivos físico de BLOB. Una nueva función intrínseca, PathName, proporciona la ruta UNC lógica del BLOB que se corresponde con cada celda de FILESTREAM de la tabla. La aplicación usa esta ruta de acceso lógica para obtener el identificador de Win32 y funcionar en los datos de BLOB usando las interfaces del sistema de archivos de Win32 normales. La función devuelve NULL si el valor de la columna FILESTREAM es NULL. 26-5-2019 Espacio de nombres de almacenamiento 19 Acceso al sistema de archivos transaccionales Una nueva función intrínseca, GET_FILESTREAM_TRANSACTION_CONTEXT(), proporciona el token que representa la transacción actual a la que la sesión está asociada. Se debe haber iniciado la transacción y no haberse anulado ni confirmado todavía. Al obtener un token, la aplicación enlaza las operaciones de transmisión por secuencias del sistema de archivos FILESTREAM con una transacción iniciada. La función devuelve NULL en caso de no haber ninguna transacción explícitamente iniciada. Se deben cerrar todos los identificadores de archivo antes de que la transacción se confirme o se anule. Si un identificador se deja abierto más allá del ámbito de transacción, las lecturas adicionales frente al identificador producirán un error; las escrituras adicionales frente al identificador tendrán éxito pero los datos reales no se escribirán en el disco. De igual forma, si la base de datos o la instancia del Motor de base de datos se cierra, se invalidan todos los identificadores abiertos. Durabilidad transaccional Con FILESTREAM, al confirmar la transacción, el Motor de base de datos asegura la durabilidad de la transacción para los datos de BLOB FILESTREAM que se modifican del acceso a la transmisión por secuencias del sistema de archivos. Semántica de aislamiento La semántica de aislamiento se rige por los niveles de aislamiento de transacción del Motor de base de datos. Se admite el nivel de aislamiento de lectura confirmada para Transact-SQLTransact-SQL y el acceso al sistema de archivos. Se admiten operaciones de lectura repetibles, así como serializables y aislamientos de instantáneas. No se admite la lectura de datos sucios. Las operaciones de apertura de acceso al sistema de archivos no esperan ningún bloqueo. En su lugar, se produce un error inmediato de las operaciones de apertura si no pueden obtener acceso a los datos debido al aislamiento de transacción. Se produce un error en las llamadas de API de transmisión por secuencias con ERROR_SHARING_VIOLATION si la operación de apertura no puede continuar debido a la infracción de aislamiento. Para permitir que se realicen actualizaciones parciales, la aplicación puede emitir un control FS de dispositivo (FSCTL_SQL_FILESTREAM_FETCH_OLD_CONTENT) para capturar el contenido anterior en el archivo al que hace referencia el identificador abierto. Esto desencadenará una copia de contenido antiguo de servidor. Para un mejor rendimiento de la aplicación, y para evitar encontrarse con posibles tiempos de espera mientras trabaja con archivos muy grandes, recomendamos que use E/S asincrónica. Si se emite FSCTL una vez que se haya escrito en el identificador, se conservará la última operación de escritura y se perderán las escrituras anteriores realizadas en el identificador. Cuando una API del sistema de archivos no puede abrir un archivo a causa de una infracción de aislamiento, se devuelve una excepción ERROR_SHARING_VIOLATION. Esta infracción de aislamiento se produce cuando dos transacciones intentan acceder al mismo archivo. El resultado de la operación de acceso depende del modo en el que se abrió el archivo y de la versión de SQL Server en la que se ejecute la transacción. En la tabla siguiente se explican resumidamente los posibles resultados de dos transacciones que están accediendo al mismo archivo. 26-5-2019 API del sistema de archivos y niveles de aislamiento admitidos 20 Transacción 2 Resultado en SQL Server 2008 Resultado en SQL Server 2008 R2 y versiones posteriores Abrir para lectura. Abrir para lectura. Ambas son correctas. Ambas son correctas. Abrir para lectura. Abrir para escritura. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en las operaciones de lectura realizadas en la transacción 1. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en las operaciones de lectura realizadas en la transacción 1. Abrir para escritura. Abrir para lectura. Se produce un error en la operación de apertura de la transacción 2 con una excepción Ambas son correctas. ERROR_SHARING_VIOLATION. Abrir para escritura. Abrir para escritura. Se produce un error en la operación de apertura de Se produce un error en la operación de apertura de la transacción 2 con una excepción la transacción 2 con una excepción ERROR_SHARING_VIOLATION. ERROR_SHARING_VIOLATION. Abrir para lectura. Abrir para SELECT. Ambas son correctas. Ambas son correctas. Abrir para lectura. Abrir para UPDATE o DELETE. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en las operaciones de lectura realizadas en la transacción 1. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en las operaciones de lectura realizadas en la transacción 1. Abrir para escritura. Abrir para SELECT. La transacción 2 se bloquea hasta que la transacción 1 se confirme o finalice la transacción. O bien, se Ambas son correctas. agota el tiempo de espera de bloqueo de la transacción. Abrir para escritura. Abrir para UPDATE o DELETE. La transacción 2 se bloquea hasta que la transacción 1 se confirme o finalice la transacción. O bien, se agota el tiempo de espera de bloqueo de la transacción. La transacción 2 se bloquea hasta que la transacción 1 se confirme o finalice la transacción. O bien, se agota el tiempo de espera de bloqueo de la transacción. Abrir para SELECT. Abrir para lectura. Ambas son correctas. Ambas son correctas. Abrir para SELECT. Abrir para escritura. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en la transacción 1. Ambas son correctas. Las operaciones de escritura de la transacción 2 no influyen en la transacción 1. Abrir para UPDATE o DELETE. Abrir para lectura. Se produce un error en la operación de apertura de la transacción 2 con una excepción Ambas son correctas. ERROR_SHARING_VIOLATION. Abrir para UPDATE o DELETE. Abrir para escritura. Se produce un error en la operación de apertura de Se produce un error en la operación de apertura de la transacción 2 con una excepción la transacción 2 con una excepción ERROR_SHARING_VIOLATION. ERROR_SHARING_VIOLATION. Abrir para SELECT con REPEATABLE READ. Abrir para lectura. Ambas son correctas. Ambas son correctas. 26-5-2019 Transacción 1 21 Transacción 1 Transacción 2 Resultado en SQL Server 2008 Abrir para SELECT con REPEATABLE READ. Abrir para escritura. Resultado en SQL Server 2008 R2 y versiones posteriores Se produce un error en la operación de apertura de Se produce un error en la operación de apertura de la transacción 2 con una excepción la transacción 2 con una excepción ERROR_SHARING_VIOLATION. ERROR_SHARING_VIOLATION. Escritura continua desde clientes remotos El acceso del sistema de archivos remoto a los datos FILESTREAM está habilitado por el protocolo Bloque de mensajes de servidor (SMB). Si el cliente es remoto, no se almacena en caché ninguna operación de escritura del lado cliente. Las operaciones de escritura siempre se enviarán al servidor. Los datos pueden se pueden almacenar en memoria caché en el servidor. Recomendamos que las aplicaciones que se están ejecutando en clientes remotos consoliden pequeñas operaciones de escritura para realizar menos operaciones de escritura mediante un tamaño de datos mayor. No se admite la creación de vistas asignadas de memoria (E/S asignada de memoria) usando un identificador FILESTREAM. Si la asignación de memoria se usa para los datos FILESTREAM, el motor de base de datos no puede garantizar la coherencia y la durabilidad de los datos o la integridad de la base de datos. Por defecto, ASP.NET permite que la carga del tamaño máximo de archivo sea de 4 MB. Si utiliza una carga de archivos ASP.NET de terceros para cargar archivos grandes y recibe una notificación de que la carga ha fallado debido al límite máximo de tamaño de archivo, el componente correspondiente no admite la carga de archivos grandes. Para anular la restricción del tamaño máximo de archivo, debe modificar el archivo web.config y habilitar el proceso de carga de archivos para manejar archivos más grandes. Con RadAsyncUpload (Telerik) puede subir rápidamente archivos de más de 4 MB. RadAsyncUpload lo ayuda a superar la limitación de carga de 4 MB en ASP.NET dividiendo los archivos grandes en trozos más pequeños y subiéndolos posteriormente. Puede controlar el tamaño de los trozos y, por lo tanto, la cantidad de solicitudes al servidor necesarias para cargar el archivo, lo que puede mejorar el rendimiento de su aplicación. Use las Herramientas de desarrollo de su navegador para monitorear el cambio en el número de solicitudes al servidor, cuando cambia el tamaño de los fragmentos, AsyncUpload divide los archivos grandes en fragmentos más 26-5-2019 pequeños para superar la limitación de ASP.NET de 4MB. 22 Vea la demostración para ver cómo el control de carga de ASP.NET de Telerik le permite cargar fácilmente archivos grandes en ASP.NET sin modificar el valor en el archivo web.config. Nuestra documentación en línea proporciona más detalles sobre cómo cargar archivos grandes con RadAsyncUpload. Tareas Relacionadas Habilitar y configurar FILESTREAM Crear una base de datos habilitada para FILESTREAM Crear una tabla para almacenar datos FILESTREAM Obtener acceso a datos FILESTREAM con Transact-SQL Crear aplicaciones cliente para datos FILESTREAM Obtener acceso a los datos FILESTREAM con OpenSqlFilestream Realizar actualizaciones parciales de los datos FILESTREAM Evitar conflictos con operaciones de base de datos en aplicaciones FILESTREAM Mover una base de datos habilitada para FILESTREAM Configurar FILESTREAM en un clúster de conmutación por error Configurar un Firewall para el acceso de FILESTREAM Contenido relacionado Compatibilidad de FILESTREAM con otras características de SQL Server Vistas de administración dinámica de secuencia de archivo y FileTable (Transact-SQL) Vistas de catálogo de secuencia de archivo y FileTable (Transact-SQL) Procedimientos almacenados del sistema de Filestream y FileTable (Transact-SQL) 26-5-2019 23 PRINCIPIO DE RESPONSABILIDAD UNICA (SINGLE RESPONSIBILITY) PRINCIPIO OPEN/CLOSED PRINCIPIO DE SUSTITUCION DE LISKOV (LISKOV SUBSTITUTION) PRINCIPIO DE SEGREGACION DE INTERFACES (INTERFACE SEGREGATION) PRINCIPIO DE INVERSION DE DEPENDENCIAS (DEPENDENCY INVERSION) 26-5-2019 REFACTORIZAR CLASE BASE CLASE DERIVADA CLASE ABSTRACTA CLASE SELLADA METODO ABSTRACTO ALTA COHESION BAJO ACOPLAMIENTO INYECCION DE DEPENDENCIA SERVICE LOCATOR HERENCIA POLIMORFISMO INMUTABILIDAD INSTANCIA 24 En DevExpress ofrecemos dos bibliotecas principales que admiten el marco MVC ASP.NET de Microsoft: Controles de DevExpress ASP.NET MVC DevExtreme MVC Controls En esta publicación, nos sumergiremos en las dos bibliotecas aparentemente similares que ofrecen controles para ASP.NET MVC. Veremos las diferencias, similitudes, ventajas, desventajas y consecuencias. Comencemos observando el desarrollo web del lado del servidor y del lado del cliente, porque esta elección será fundamental en qué biblioteca funcionará para usted. Una de las preocupaciones más importantes para la elección de una plataforma de desarrollo web es la arquitectura: la representación tradicional de la interfaz de usuario del lado del servidor o del lado del cliente. Es importante porque tiene muchas consecuencias. Recomiendo leer esta sección, pero para aquellos que no tienen tiempo, aquí hay un breve tldr: 26-5-2019 Elecciones arquitectónicas y consecuencias 25 La elección de la interfaz de usuario representada de servidor contra cliente influye en la arquitectura general de un sistema de software hasta el punto de que es recomendable analizar este asunto antes de tomar decisiones sobre bibliotecas de componentes específicos. Lado del servidor Si usa la IU representada por el servidor, esto implica que necesita un servidor lo suficientemente potente como para representar las páginas dinámicamente (por solicitud). Por lo general, esto funciona utilizando archivos de plantilla en el servidor que describen las páginas, con un modelo de extensión para insertar el contenido dinámico. Ambos marcos usan este enfoque con ASP.NET WebForms que usan páginas ASPX y ASP.NET MVC que usa archivos Razor como plantillas. Si bien los modelos de componentes son diferentes entre WebForms y MVC, ambos se representan en el entorno .NET del lado del servidor. Esto significa que se pueden usar los métodos de acceso a datos basados en .NET, la lógica de negocios escrita en C# y el estado en el lado del servidor. Incluso la representación dinámica del navegador o específica del usuario se puede realizar en el servidor, quizás dependiendo de la configuración regional del usuario o los permisos consultados desde un sistema de seguridad. También hay una brecha para ser superada con este modelo: por ejemplo, podría ser necesario escribir ciertos fragmentos de código dos veces, en C# y JavaScript, como los requeridos para realizar tareas de 26-5-2019 Este modelo permite a los programadores que se sientan como en casa en el entorno del servidor. Sin embargo, puede ser costoso con ASP.NET WebForms debido a los numerosos viajes de ida y vuelta del servidor requeridos para propósitos de representación. También hace que sea casi imposible implementar la aplicación web resultante en entornos donde un servidor podría no estar disponible, como las aplicaciones de escritorio basadas en Electron o las aplicaciones móviles basadas en Cordova. Se requiere mucho cuidado para evitar problemas de escalabilidad porque los recursos del servidor son preciosos. Además, no siempre es fácil agregar infraestructura, por ejemplo, cuando la administración de estado del lado del servidor no se ha estructurado correctamente. 26 validación de datos tanto para comentarios inmediatos del usuario en el lado del cliente como para fines de lógica de negocios del lado del servidor. Sin embargo, si está creando un sitio web público, la representación del lado del servidor es ideal para SEO (optimización de motores de búsqueda). Debido a que el contenido HTML se genera en el servidor, los motores de búsqueda como Google pueden rastrearlo, indexarlo y almacenarlo en caché. La representación del lado del cliente no puede hacer esto fácilmente sin trabajo adicional. Lado del cliente Con la IU representada por el cliente, un servidor no es estrictamente necesario. El contenido HTML y JavaScript se abre camino en el cliente de alguna manera: se puede descargar como contenido estático desde un servidor, pero también se puede implementar como un paquete de instalación para destinos de escritorio o móviles. Los componentes utilizados por el cliente para representar la IU compleja se escriben en JavaScript (o un lenguaje que se compila con JavaScript, como TypeScript), y los desarrolladores generalmente eligen los marcos de aplicación como Angular o React que los ayudan a estructurar la aplicación del lado del cliente, pero también Requieren integración con bibliotecas de componentes. Este modelo es una reminiscencia de las aplicaciones cliente/servidor simples, y solo utiliza protocolos web estándar para la comunicación de servicio y el acceso a datos. Es posible crear sistemas de aplicación basados en JavaScript de pila completa si los servicios están escritos en JavaScript (probablemente para Node.js), y debido a la naturaleza sin estado inherente de los servicios, generalmente es fácil escalar los servidores según sea necesario. En general, los requisitos de infraestructura del lado del servidor son menos complejos con esta arquitectura, por lo que las implementaciones en la nube son fáciles y permiten configuraciones de escalado automáticas. 26-5-2019 Debido a que el navegador del cliente ahora manejará el HTML y la representación de esos elementos de la interfaz de usuario, la aplicación del lado del cliente en su mayoría necesita los datos del servidor. Por lo general, deberá crear servicios web para proporcionar estos datos. Se puede utilizar la WebApi de ASP.NET MVC para implementar dichos servicios, lo que nuevamente brinda al desarrollador la oportunidad de usar mecanismos de acceso a datos basados en .NET, pero se requieren protocolos adicionales, por ejemplo, para consultar datos dinámicamente desde los servicios y transferirlos de vuelta al cliente. 27 La IU representada por el cliente es el modelo arquitectónico de elección para la mayoría de las aplicaciones web / móviles modernas y un número creciente de aplicaciones de escritorio, que incluyen Office 365, Skype y Microsoft Visual Studio Code. La mejor recomendación para tomar esta decisión es sopesar los pros y los contras cuidadosamente, ya que se aplican a sus planes, la experiencia de los desarrolladores en su equipo, su base de clientes y otros factores. Esto nos lleva a la gran diferencia entre nuestros dos controles de MVC. Mientras que ambos son controles de ASP.NET MVC del lado del servidor, los Controles de MVC de DevExtreme representarán la IU del lado del cliente debido a que envuelven los controles de JavaScript de DevExtreme del lado del cliente en los controles de ASP.NET MVC. Por otro lado, los controles DevExpress ASP.NET MVC representarán el HTML del lado del servidor y luego lo entregarán al cliente. Veamos ahora cómo estas dos bibliotecas comenzaron a comprender nuestras motivaciones para crearlas. Historia 1. DevExpress ASP.NET MVC Controls En abril de 2009, Microsoft lanzó ASP.NET MVC y muchos desarrolladores se interesaron rápidamente en este nuevo e interesante marco web. Muchos de nuestros clientes solicitaron que proporcionáramos controles de UI para ASP.NET MVC. En ese momento, nuestra biblioteca ASP.NET WebForms proporcionaba controles sólidos, estables y ricos en funciones. Así que tomamos la decisión de tomar la API, las características y la representación de nuestros controles de formularios web de ASP.NET y proporcionarlos como controles MVC nativos. Este enfoque tuvo la ventaja de ahorrar tiempo y ofrecerle un conjunto sólido de controles con muchas funciones. Casi al mismo tiempo, JavaScript también estaba ganando popularidad entre los desarrolladores web... 26-5-2019 En 2010, presentamos los controles DevExpress ASP.NET MVC como beta con solo cinco controles. Luego, durante los siguientes siete años, con cada lanzamiento continuamos aumentando los controles, las funciones y el rendimiento. Hoy en día, el conjunto de controles DevExpress MVC es sólido y tiene más de 65 controles. 28 2. Controles DevExtreme MVC Desde finales de la década de 2000, la popularidad de JavaScript ha seguido aumentando. Y hace muchos años, el desarrollo web cambió decididamente hacia tecnologías basadas en JavaScript del lado del cliente como jQuery, Node.js, Angular, Backbone, React, etc. sin razón, porque el modelo de desarrollo del lado del cliente ofrece beneficios. Muchos de nuestros clientes web existentes nos pidieron que proporcionáramos controles de JavaScript que se representaban exclusivamente en el lado del cliente. Por lo tanto, en 2011, asignamos a un grupo independiente de nuestros desarrolladores la tarea de crear un conjunto de controles de JavaScript totalmente del lado del cliente que tuvieran características similares a las de nuestros controles del lado del servidor. Después de un año de desarrollo, la biblioteca DevExtreme nació en 2012. Hemos continuado desarrollando esta biblioteca del lado del cliente desde entonces para proporcionar más controles, características, estabilidad y soporte para otros marcos del lado del cliente. Luego, alrededor de 2014, algunos de nuestros clientes web expresaron la necesidad de un conjunto de controles MVC que utilizaron el enfoque de representación del cliente. Así que decidimos envolver los controles de JavaScript DevExtreme del lado del cliente como controles MVC del lado del servidor. En junio de 2016, los controles DevExtreme MVC se introdujeron como un avance tecnológico comunitario (CTP). Ahora, después de varias versiones en las que el equipo ha trabajado duro para mejorar los controles DevExtreme MVC, proporcionan más de 65 controles, características y una gran integración con ASP.NET MVC (y ASP.NET Core). ¿Qué es lo correcto para ti? Si bien las consideraciones arquitectónicas deberían ayudar a impulsar la mayoría de los proyectos en su dirección preferida, también es posible comparar nuestras bibliotecas en base a méritos técnicos específicos. Es posible que la siguiente descripción general no sea exhaustiva. No dude en preguntar sobre los temas que le faltan y ampliaremos la publicación. ¿Qué recomiendas para mi escenario? ¿Cuáles son las principales diferencias entre los dos productos? 26-5-2019 Recientemente, hemos recibido preguntas de desarrolladores que preguntan: 29 Para ayudarlo a decidir, lea las siguientes cuatro secciones que comparan las fortalezas y debilidades de estas dos bibliotecas: 1. ¿Qué está incluido y qué falta? A primera vista, parece que ambas bibliotecas tienen más de 65 juegos de controles MVC y proporcionan controles clave como Cuadrícula de datos, Gráficos, Editores, Navegación, etc. Sin embargo, no todos los controles están disponibles en ambas bibliotecas y esto puede ser Un factor decisivo clave. Por ejemplo, si necesita controles de hoja de cálculo, cinta de opciones o RichEdit, estos solo estarán disponibles en la suite de Controles DevExpress ASP.NET MVC. Aquí hay un desglose de qué controles están disponibles en cada biblioteca: DevExpress ASP.NET MVC Controls (Server-Side) DevExtreme MVC Controls (Client-Side) Grid PivotGrid TreeList, TreeView Scheduler TreeList, TreeView Scheduler Navigation Controls Editor Controls Charts & Gauges Forms Reporting Suite RichEdit HTML Editor Ribbon 26-5-2019 Spreadsheet 30 FileManager CardView Vertical Grid Docking Suite Spell Checker ActionSheet, ScrollView, SlideOut Bar, Circular, Linear Gauge Map, TreeMap, VectorMap RangeSelector Bullet Sparkline FilterBuilder Integrated into other controls Box, ResponsiveBox Toast 2. Representación Pros / Contras Los controles DevExpress ASP.NET MVC proporcionan representación del lado del servidor. Pros: Sólido con (muchos más) años de lanzamientos. Controles más potentes disponibles. Contras: Windows IIS servidores de alojamiento. Las devoluciones de llamada, sin embargo, los datos se envían a través de formularios nativos, formularios MVC o formularios Ajax MVC y se puede acceder a ellos mediante la acción de un controlador. Pros: emiten un marcado semántico que es representado por el navegador del cliente. La interacción del usuario es más rápida. Integración más estrecha con el marco MVC de ASP.NET. Contras: para la interactividad y la personalización en tiempo de ejecución, tendrás que lidiar con los widgets principales de JavaScript. A. Desempeño 26-5-2019 Los controles DevExtreme MVC proporcionan una representación del lado del cliente. 31 Los controles DevExtreme MVC tienen algunas ventajas menores en términos de rendimiento debido a su representación del lado del cliente. Principalmente, la interacción del usuario se sentirá más receptiva. Por ejemplo, al agrupar filas en la cuadrícula de datos de DevExtreme MVC, encontrará que solo recuperará los datos de las filas que necesita actualizar para mostrar el diseño agrupado. En la imagen de las herramientas del navegador a continuación, podemos ver que DevExtreme MVC Grid solo obtiene JSON para las filas de datos del servidor y Grid mostrará y actualizará la interfaz de usuario en consecuencia: 26-5-2019 Sin embargo, DevExpress ASP.NET MVC GridView realizará una actualización parcial de la página. Lo que significa que recuperará el HTML para el nuevo diseño de agrupación del servidor y luego los entregará al cliente y los procesará en su lugar. Una actualización parcial de la página es similar a la siguiente cuando se envía a través de la cadena de solicitud / respuesta: 32 Las devoluciones de llamada requieren un esfuerzo adicional del servidor para volver a representar estos fragmentos de código HTML para (potencialmente) un gran número de clientes y, por lo tanto, aumentar el volumen de datos. Sin embargo, en una conexión rápida, es probable que no note una gran diferencia en la velocidad. B. Procesamiento semántico de HTML 26-5-2019 Como se mencionó, los controles DevExtreme MVC proporcionan un marcado semántico cuando se procesan (haga clic en la imagen para una versión más grande): 33 26-5-2019 Los controles de DevExpress ASP.NET MVC más grandes, como el Grid, utilizan un diseño basado en tabla para la representación. Sin embargo, los controles de navegación (paginador, menú, barra de navegación, etc.) tienen una representación semántica que se basa en listas, divs, etc. 34 Ambos enfoques de representación cumplen con los estándares HTML5. 3. Enlace de datos Las Extensiones DevExpress ASP.NET MVC son controles del lado del servidor y, por lo tanto, pueden enlazarse fácilmente a cualquier capa de datos .NET del lado del servidor. Sin embargo, el enlace del lado del cliente no es compatible con estos controles del lado del servidor. Los controles DevExtreme MVC proporcionan varias formas de enlazar controles conscientes de los datos en el lado del cliente. Estos controles también proporcionan una capa de datos del lado del servidor que le permite enlazar en el lado del servidor también. Este mecanismo no cambia las consideraciones arquitectónicas sobre el enlace de datos, simplemente proporciona servicios web para el acceso a los datos de forma automática, según los orígenes de datos existentes del lado del servidor. Ambas bibliotecas son compatibles con .NET Framework 4+ y el mismo conjunto de navegadores. 26-5-2019 4. Soporte de .NET Core 35 Sin embargo, los controles DevExtreme MVC también son compatibles con ASP.NET Core 1.0.1 y versiones posteriores (incluyendo 1.1, 2.0+), mientras que los controles DevExpress ASP.NET MVC no lo hacen. Si bien no es parte de esta comparación, los nuevos controles DevExpress ASP.NET Bootstrap Core son compatibles exclusivamente con el marco Core de ASP.NET. Utilizar juntos Tenga en cuenta que si elige el marco completo clásico ASP.NET MVC (no ASP.NET Core), puede usar ambos controles MVC juntos para complementarse entre sí. Por ejemplo, puede usar DevExpress ASP.NET MVC Spreadsheet y DevExtreme MVC Grid en el mismo proyecto ASP.NET MVC. ¡Estas dos bibliotecas MVC también se ofrecen en la Suscripción DevExpress ASP.NET! La decisión es tuya Para resumir, las dos suites de control MVC le ofrecen muchos controles de interfaz de usuario excelentes y también se pueden usar juntos. Sin embargo, si necesita elegir entre ellos, le ofreceré mis observaciones como administrador del programa web que interactuó con muchos desarrolladores: Los desarrolladores que prefieren la representación del lado del cliente suelen elegir los controles DevExtreme MVC Los fans de C # y la representación del lado del servidor normalmente eligen los controles DevExpress ASP.NET MVC Con suerte, la información anterior le ayudará a elegir. Si no es así, deje un comentario a continuación sobre cómo puedo ayudar. Autor: 26-5-2019 Email: [email protected] 36